From 2efa5c1976510612fc667c6217886e17680bd97c Mon Sep 17 00:00:00 2001 From: rzcoder Date: Sun, 16 Nov 2014 21:37:17 +0500 Subject: [PATCH 01/11] minor code fixes --- src/NodeRSA.js | 6 +++--- src/libs/rsa.js | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/NodeRSA.js b/src/NodeRSA.js index 32a09fe..6e998c7 100644 --- a/src/NodeRSA.js +++ b/src/NodeRSA.js @@ -72,9 +72,9 @@ module.exports = (function() { if (/^\s*-----BEGIN RSA PRIVATE KEY-----\s*([A-Za-z0-9+/=]+\s*)+-----END RSA PRIVATE KEY-----\s*$/g.test(pem)) { this.$loadFromPrivatePEM(pem, 'base64'); } else if (/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=]+\s*)+-----END PUBLIC KEY-----\s*$/g.test(pem)) { - this.$loadFromPublicPEM(pem, 'base64'); + this.$loadFromPublicPKCS8(pem, 'base64'); } else - throw Error('Invalid PEM format'); + throw Error('Invalid key format'); this.$recalculateCache(); }; @@ -111,7 +111,7 @@ module.exports = (function() { * * @param publicPEM {string} */ - NodeRSA.prototype.$loadFromPublicPEM = function(publicPEM, encoding) { + NodeRSA.prototype.$loadFromPublicPKCS8 = function(publicPEM, encoding) { var pem = publicPEM .replace('-----BEGIN PUBLIC KEY-----','') .replace('-----END PUBLIC KEY-----','') diff --git a/src/libs/rsa.js b/src/libs/rsa.js index 7dbb9e1..0566a45 100644 --- a/src/libs/rsa.js +++ b/src/libs/rsa.js @@ -45,9 +45,9 @@ 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') + sha1: new Buffer('3021300906052b0e03021a05000414', 'hex'), + sha256: new Buffer('3031300d060960864801650304020105000420', 'hex'), + md5: new Buffer('3020300c06082a864886f70d020505000410', 'hex') }; exports.BigInteger = BigInteger; @@ -351,9 +351,7 @@ module.exports.Key = (function() { filled[0] = 1; filled[filled.length - 1] = 0; - var res = Buffer.concat([filled, data]); - - return res; + return Buffer.concat([filled, data]); }; /** From 2ce4b9e545624fcb2182aadd66fb3089284b1044 Mon Sep 17 00:00:00 2001 From: BAM5 Date: Tue, 18 Nov 2014 18:29:10 -0500 Subject: [PATCH 02/11] Major overhaul to add schemes. Everything should be backwards compatible. --- README.md | 45 ++- src/NodeRSA.js | 124 ++++--- src/libs/jsbn.js | 25 +- src/libs/rsa.js | 902 ++++++++++++++++++++++++++++++++++++++--------- test/tests.js | 224 ++++++------ 5 files changed, 986 insertions(+), 334 deletions(-) diff --git a/README.md b/README.md index 8917243..4089b3e 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,6 @@ 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 ```javascript @@ -119,6 +116,13 @@ Return max data size for encrypt in bytes. ### Encrypting/decrypting +*As of v0.1.55 the default encryption scheme is RSAES-OAEP using sha1 and mgf1. +PKCS1 is still available with the following configuring* + +```javascript +key.schemeEncryption = new NodeRSA.RSA.PKCS1.Default; +``` + ```javascript key.encrypt(buffer, [encoding], [source_encoding]); ``` @@ -135,6 +139,14 @@ Return decrypted data.
**encoding** - encoding for result string. Can also take `'buffer'` for raw Buffer object, or `'json'` for automatic JSON.parse result. Default `'buffer'`. ### Signing/Verifying + +*As of v0.1.55 the default signature scheme is RSASSA-PSS using sha1. +PKCS1 is still available with the following configuring* + +```javascript +key.schemeSignature = new NodeRSA.RSA.PKCS1.Default; +``` + ```javascript key.sign(buffer, [encoding], [source_encoding]); ``` @@ -149,6 +161,33 @@ Return result of check, `true` or `false`.
**source_encoding** - same as for `encrypt` method.
**signature_encoding** - encoding of given signature. May be `'buffer'`, `'binary'`, `'hex'` or `'base64'`. Default `'buffer'`. +### Changing Encryption/Signature schemes. + +Schemes are the way RSA packs up it's data before encrypting/decrypting/signing/verifying and there are a few ways that it can do it. The most common are RSAES-OAEP, RSASSA-PSS, RSAES-PKCS1-v1_5(encryption/decryption), and RSASSA-PKCS1-v1_5(signing/verifying). +As of v0.1.55 these 4 mentioned schemes are included in the package with the ability to easily add more later and by users. See below for how to configure NodeRSA to use these different schemes. + +*Note: The default encryption / signature schemes have been changed from PKCS1 to the more secure OAEP(encryption) and PSS(signature) schemes as per the recommendations of the spec, this can be breaking.* + +```javascript +key.schemeSignature = NodeRSA.RSA.PSS.Default; // This is an object that has been automatically instantiated and has the default settings for PSS signing. +key.schemeSignature = NodeRSA.RSA.PKCS1.Default; // This is an object that has been automatically instantiated and has the default settings for PKCS1 signing/encrypting. + +key.schemeEncryption = NodeRSA.RSA.OAEP.Default; // This is an object that has been automatically instantiated and has the default settings for OAEP encrypting. +key.schemeEncryption = NodeRSA.RSA.PKCS1.Default; // This is an object that has been automatically instantiated and has the default settings for PKCS1 signing/encrypting. +``` +To change schemes between the various defaults, each provided scheme class has a property named "Default" that provides a scheme object with the default settings for that scheme. + +```javascript +var pssMD5Scheme = new NodeRSA.RSA.PSS({ + // The options a scheme accepts should be documented in the scheme definition. + hash: "md5", // Use a different type of hashing function instead of sha1 + mgf: customMaskGenerationFunction // Use a custom mask generation function that accepts 3 parameters (seed, maskLength, hashFunction) +}); +key.schemeSignature = pssMD5Scheme; +key2.schemeSignature = pssMD5Scheme; +``` +Schemes can also be initialized with custom options and can be shared between keys. + ## Contributing Questions, comments, bug reports, and pull requests are all welcome. diff --git a/src/NodeRSA.js b/src/NodeRSA.js index 32a09fe..5c3cf2b 100644 --- a/src/NodeRSA.js +++ b/src/NodeRSA.js @@ -8,7 +8,6 @@ */ var rsa = require('./libs/rsa.js'); -var crypt = require('crypto'); var ber = require('asn1').Ber; var _ = require('lodash'); var utils = require('./utils'); @@ -21,17 +20,21 @@ module.exports = (function() { * @constructor */ function NodeRSA(key, options) { - if (! this instanceof NodeRSA) { + if (! this instanceof NodeRSA) return new NodeRSA(key, options); - } this.keyPair = new rsa.Key(); this.$cache = {}; - - this.options = _.merge({ - signingAlgorithm: 'sha256', - environment: utils.detectEnvironment() - }, options || {}); + + if(options){ + console.warn("There are no more options and the parameter is deprecated. It may be removed in the future."); + if(options.signingAlgorithm){ + console.warn("options.signingAlgorithm has been removed. In order to change the signature hashing algorithm create a signature scheme with the appropriate options and set NodeRSA.schemeSignature to that object. Schemes are defined in src/libs/rsa.js Default is RSASSA-PSS (RSA.PSS) with the hashing function sha1"); + this.schemeSignature = new rsa.PKCS1({hash: options.signingAlgorithm}); + } + if(options.environment) + console.warn("options.environment has been removed since all encryption is now done through the rsa.js library"); + } if (Buffer.isBuffer(key) || _.isString(key)) { this.loadFromPEM(key); @@ -39,6 +42,8 @@ module.exports = (function() { this.generateKeyPair(key.b, key.e); } } + + NodeRSA.RSA = rsa; /** * Generate private/public keys pair @@ -155,6 +160,25 @@ module.exports = (function() { return !(this.keyPair.n || this.keyPair.e || this.keyPair.d); }; + /* + * In order to use a different encryption/signing/padding scheme then the default (OAEP for encrypting and PSS for signing) + * create a new scheme object with desired options and set the appropriate variable to that scheme object. + * Scheme classes are located in rsa.js and new ones can be easily added. + */ + Object.defineProperties(NodeRSA.prototype, { + schemeEncryption: { + enumerable: true, + get: function(){ return this.keyPair.schemeEncryption; }, + set: function(scheme){ this.keyPair.schemeEncryption = scheme; } + }, + + schemeSignature: { + enumerable: true, + get: function(){ return this.keyPair.schemeSignature; }, + set: function(scheme){ this.keyPair.schemeSignature = scheme; } + } + }); + /** * Encrypting data method * @@ -163,8 +187,14 @@ 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){ + buffer = this.$getDataForEcrypt(buffer, source_encoding); + try{ + var res = this.keyPair.encrypt(buffer); + } catch(e){ + console.warn(e.message); + return null; + } if (encoding == 'buffer' || !encoding) { return res; @@ -176,13 +206,20 @@ module.exports = (function() { /** * Decrypting data method * - * @param buffer {Buffer} - buffer for decrypting - * @param encoding - encoding for result string, can also take 'json' or 'buffer' for the automatic conversion of this type - * @returns {Buffer|object|string} + * @param buffer {Buffer} - buffer for decrypting, or base64 string + * @param encoding - encoding for result string, can also take 'json' or 'buffer' for the automatic conversion of this type. Default "utf8" + * @returns {string|Buffer|object} */ - NodeRSA.prototype.decrypt = function(buffer, encoding) { + NodeRSA.prototype.decrypt = function(buffer, encoding){ buffer = _.isString(buffer) ? new Buffer(buffer, 'base64') : buffer; - return this.$getDecryptedData(this.keyPair.decrypt(buffer), encoding); + var clone = new Buffer(buffer.length); + buffer.copy(clone); + try{ + return this.$getDecryptedData(this.keyPair.decrypt(clone), encoding); + } catch(e){ + console.warn(e.message); + return null; + } }; /** @@ -193,24 +230,21 @@ 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) { - if (!this.isPrivate()) { - throw Error("It is not private key"); - } - - 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); - } + NodeRSA.prototype.sign = function(buffer, encoding, source_encoding){ + if(!this.isPrivate()) throw Error("It is not private key"); + + // Move all the lifting to rsa.js module due to its wider options. + try{ + var res = this.keyPair.sign(this.$getDataForEcrypt(buffer, source_encoding)); + } catch(e){ + console.warn(e.message); + return null; + } + + if(encoding && encoding != 'buffer') + return res.toString(encoding); + + return res; }; /** @@ -222,20 +256,16 @@ 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) { - 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); - } + 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); + try{ + return this.keyPair.verify(this.$getDataForEcrypt(buffer, source_encoding), signature, signature_encoding); + } catch(e){ + console.warn(e.message); + return false; + } }; NodeRSA.prototype.getPrivatePEM = function () { diff --git a/src/libs/jsbn.js b/src/libs/jsbn.js index eee75a2..eb92939 100644 --- a/src/libs/jsbn.js +++ b/src/libs/jsbn.js @@ -34,6 +34,9 @@ /* * Added Node.js Buffers support * 2014 rzcoder + * + * Extended bnToBuffer to allow length specifying. + * 2014 BAM5 */ var crypt = require('crypto'); @@ -812,12 +815,26 @@ function bnToByteArray() { /** * return Buffer object - * @param trim {boolean} slice buffer if first element == 0 + * @param trimOrSize {boolean | uint} slice buffer if first element == 0 * @returns {Buffer} */ -function bnToBuffer(trim) { - var res = new Buffer(this.toByteArray()); - return trim && res[0] === 0 ? res.slice(1) : res; +function bnToBuffer(trimOrSize) { + if(trimOrSize === true || trimOrSize === undefined){ + var res = new Buffer(this.toByteArray()); + return trimOrSize && res[0] === 0 ? res.slice(1) : res; + } else{ + var res = new Buffer(this.toByteArray()); + if(res.length > trimOrSize){ + for(var i=0; i this.encryptedDataLength) throw new Error("The returned encrypted message is too long. This is most likely an issue with the scheme that is being used."); + results.push(encrypted); } return Buffer.concat(results); }; /** - * Return the PKCS#1 RSA decryption of buffer - * @param buffer {Buffer} + * Return the decryption of buffer that uses the given scheme, if scheme is not set then this.schemeEncryption is presumed to be the scheme used in this message. + * @param buffer {Buffer} This buffer may get mangled to save memory depending on the scheme function used. * @returns {Buffer} */ - RSAKey.prototype.decrypt = function (buffer) { - if (buffer.length % this.encryptedDataLength > 0) - throw Error('Incorrect data or key'); + RSAKey.prototype.decrypt = function (buffer, scheme){ + if(this.usedSign) console.warn("It is against recommendations to use the same private key for encryption and signatures.") + this.usedDecrypt = true; + + if(buffer.length % this.encryptedDataLength !== 0) + throw Error("Incorrect data or key, the buffer must have a length that is the multiple of this key's length"); + + scheme = scheme || this.schemeEncryption; var result = []; + var chunkCount = buffer.length / this.encryptedDataLength; - var offset = 0; - var length = 0; - - var buffersCount = buffer.length / this.encryptedDataLength; - - for (var i = 0; i < buffersCount; i++) { - offset = i * this.encryptedDataLength; - 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)); - } + for(var i = 0; i < chunkCount; i++) + result.push(scheme.decrypt( + this, // Key + buffer.slice( // Cut out a chunk of the buffer to decrypt with Key + i*this.encryptedDataLength, // Beginning of chunk + (i+1)*this.encryptedDataLength // End of chunk + ) + )); 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 (data, hashAlgorithm) { + if(this.usedDecrypt) console.warn("It is against recommendations to use the same private key for encryption and signatures.") + if(this.hashAlgorithm){ + console.warn("! hashAlgorithm parameter is deprecated and may be removed in future versions."); + if(!RSA.PKCS1.Temp) RSA.PKCS1.Temp = new RSA.PKCS1(); + RSA.PKCS1.Temp.options.hash = hashAlgorithm; + var signature = RSA.PKCS1.Temp.sign(this, data); + } else + var signature = this.schemeSignature.sign(this, data); + + this.usedSign = true; + return signature; }; RSAKey.prototype.verify = function (buffer, signature, signature_encoding, hashAlgorithm) { - - if (signature_encoding) { + 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'); + + if(hashAlgorithm){ + console.warn("! hashAlgorithm parameter is deprecated and may be removed in future versions."); + if(!RSA.PKCS1.Temp) RSA.PKCS1.Temp = new RSA.PKCS1(); + RSA.PKCS1.Temp.options.hash = hashAlgorithm; + return RSA.PKCS1.Temp.verify(this, buffer, signature); + } + + return this.schemeSignature.verify(this, buffer, signature); }; - + + Object.defineProperty(RSAKey.prototype, 'keySize', { get: function() { return this.cache.keyBitLength; } }); @@ -312,110 +324,648 @@ module.exports.Key = (function() { }); Object.defineProperty(RSAKey.prototype, 'maxMessageLength', { - get: function() { return this.encryptedDataLength - 11; } + get: function() { return this.schemeEncryption.maxMessageLength(this); } }); + Object.defineProperties(RSAKey.prototype, { + schemeEncryption: { + enumerable: true, + get: function(){ return this._schemeEncryption; }, + set: function(scheme){ + if(RSA.isEncryptionScheme(scheme)) this._schemeEncryption = scheme; + else throw new Error("Provided object is not an encryption scheme"); + } + }, + + schemeSignature: { + enumerable: true, + get: function(){ return this._schemeSignature; }, + set: function(scheme){ + if(RSA.isSignatureScheme(scheme)) this._schemeSignature = scheme; + else throw new Error("Provided object is not a signature scheme"); + } + } + }); + /** * Caching key data */ RSAKey.prototype.$$recalculateCache = function () { - this.cache = this.cache || {}; - // Bit & byte length - this.cache.keyBitLength = this.n.bitLength(); - if (this.cache.keyBitLength % 2 == 1) { - this.cache.keyBitLength = this.cache.keyBitLength + 1; - } - - this.cache.keyByteLength = (this.cache.keyBitLength + 6) >> 3; + this.cache = this.cache || {}; + // Bit & byte length + this.cache.keyBitLength = this.n.bitLength(); + this.cache.keyBitLength += this.cache.keyBitLength % 2; + 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 + ')'); - } + return RSAKey; +})(); - 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); +RSA.isEncryptionScheme = function(object){ + return ("maxMessageLength" in object && "encrypt" in object && "decrypt" in object); +}; - // 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); +RSA.isSignatureScheme = function(object){ + return ("sign" in object && "verify" in object); +}; - return new BigInteger(ba); - }; +RSA.OAEP = (function(){ + + // OAEP Padding Scheme + + /* + * Retuns an object that OAEP pads, encodes, and encrypts buffer objects with the options specified in options. + * + * options [Object] Options for the encoding (The defaults should be used unless in special use cases and user knows what they're doing) + * ├>label [Buffer] Value to pass to the $$eme_oaep_encode and $$eme_oaep_decode functions as the L parameter. + * ├>hash [String] The hashing function to use when encoding and creating checksums (Default: "sha1")(only SHA-1 and SHA-256/384/512 are recommended)(L parameter max size depends on hashing function, however sha1 and sha256's size limit are too large to touch (TBytes) so length checking is NOT implemented) + * └>mgf [function] The mask generation function (Default: OAEP.$$eme_oaep_mgf1) + * + * + * @param {Object} options + * @returns {RSA.OAEP} + */ + var OAEP = function(options){ + if(!options) options = {}; + this.options = options; + this.count = 0; + }; + + OAEP.prototype.maxMessageLength = function(key){ + return key.encryptedDataLength - 2*RSA.$$digestLength[this.options.hash] - 2; + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-7.1.1 + * + * @param {RSA.Key} key + * @param {Buffer} message + * @returns {Buffer} + */ + OAEP.prototype.encrypt = function(key, message){ + var m = OAEP.$$eme_oaep_encode(message, this.options.label, key.encryptedDataLength, this.options); + m = new BigInteger(m); + m = key.$doPublic(m); + m = m.toBuffer(key.encryptedDataLength); + return m; + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-7.1.2 + * + * @param {RSA.Key} key + * @param {Buffer} encMessage + * @returns {Buffer} + */ + OAEP.prototype.decrypt = function(key, encMessage){ + if(encMessage.length != key.encryptedDataLength || encMessage.length < 2*RSA.$$digestLength[this.options.hash] + 2) + throw new Error("Decryption Error"); + + encMessage = new BigInteger(encMessage); + encMessage = key.$doPrivate(encMessage); + encMessage = encMessage.toBuffer(key.encryptedDataLength); + + return OAEP.$$eme_oaep_decode(encMessage, this.options.label, this.options); + }; + + + + + + /* + * 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 + */ + OAEP.$$eme_oaep_mgf1 = function(seed, maskLength, hashFunction){ + hashFunction = hashFunction || "sha1"; + var hLen = RSA.$$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) { + 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); + }; + + /* + * Encode message with OAEP format + padding + * + * https://tools.ietf.org/html/rfc3447#section-7.1.1 + * + * Parameters: + * M [Buffer] The message bytes to be encoded + * L [Buffer] Label to associate with this message. Usually left blank. (Default: '') + * emLen [int] Size of the returned encoded message + * options [Object] Options for the encoding (The defaults should be used unless in special use cases and user knows what they're doing) + * ├>hash [String] The hashing function to use when encoding and creating checksums (Default: "sha1") (L parameter max size depends on hashing function, however sha1 and sha256's size limit are too large to touch so length checking is NOT implemented) + * └>mgf [function] The mask generation function (Default: $$eme_oaep_mgf1) + */ + OAEP.$$eme_oaep_encode = function(M, L, emLen, options){ + // Prepare options + if(!options) options = {}; + options.hash = options.hash || "sha1"; + options.mgf = options.mgf || OAEP.$$eme_oaep_mgf1; + + var hLen = RSA.$$digestLength[options.hash]; + + // Make sure we can put message into an encoded message of emLen bytes + if(M.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)+")"); + + L = L || new Buffer(0); + var lHash = crypt.createHash(options.hash); + lHash.end(L); + lHash = lHash.read(); + + var PS = new Buffer(emLen - M.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, M]); + var seed = crypt.randomBytes(hLen); + + // mask = dbMask + var mask = options.mgf(seed, DB.length, options.hash); + // XOR DB and dbMask together. + for(var i = 0; ihash [String] The hashing function to use when decoding and checking checksums (Default: "sha1") (L parameter max size depends on hashing function, however sha1 and sha256's size limit are too large to touch so length checking is NOT implemented) + * └>mgf [function] The mask generation function (Default: $$eme_oaep_mgf1) + */ + OAEP.$$eme_oaep_decode = function(EM, L, options){ + // Prepare options + if(!options) options = {}; + options.hash = options.hash || "sha1"; + options.mgf = options.mgf || OAEP.$$eme_oaep_mgf1; + + var hLen = RSA.$$digestLength[options.hash]; + + // Check to see if EM is a properly encoded OAEP message + if(EM.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 = EM.slice(1, hLen+1); // seed = maskedSeed + var DB = EM.slice(1+hLen); // DB = maskedDB + + var mask = options.mgf(DB, hLen, options.hash); // seedMask + // XOR maskedSeed and seedMask together to get the original seed. + for(var i = 0; 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; +RSA.PSS = (function(){ + + + // PSS Signature Scheme + + /* + * Retuns an object that PSS signs buffer objects with the options specified in the options parameter. + * + * options [Object] Options for the encoding (The defaults should be used unless in special use cases and user knows what they're doing) + * ├>hash [String] The hashing function to use when generating signatures (Default: "sha1")(only SHA-1 and SHA-256/384/512 are recommended) + * ├>mgf [function] The mask generation function (Default: RSA.OAEP.$$eme_oaep_mgf1) + * └>sLen [uint] The length of the salt to generate. (default = 20) + * + * @param {Object} options + * @returns {RSA.PSS} + */ + var PSS = function(options){ + if(!options) options = {}; + this.options = options; + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-8.1.1 + * + * @param {RSA.Key} key + * @param {Buffer} data + * @returns {Buffer} The generated signature + */ + PSS.prototype.sign = function(key, data){ + var encoded = PSS.$$emsa_pss_encode(data, key.keySize - 1, this.options); + encoded = new BigInteger(encoded); + return key.$doPrivate(encoded).toBuffer(key.encryptedDataLength); + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-8.1.2 + * + * @param {RSA.Key} key + * @param {Buffer} data + * @param {Buffer} signature + * @returns {Boolean} True if signature is valid, false otherwise. + */ + PSS.prototype.verify = function(key, data, signature){ + signature = new BigInteger(signature); + + var emLen = Math.ceil((key.keySize - 1)/8); + signature = key.$doPublic(signature).toBuffer(emLen); + + return PSS.$$emsa_pss_verify(data, signature, key.keySize -1, this.options); + }; + + + + + + /* + * 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) + * options [Object] An object that contains the following keys that specify certain options for encoding. + * ├>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) + * + * @returns {Buffer} The encoded message + */ + PSS.$$emsa_pss_encode = function(M, emBits, options){ + if(!options) options = {}; + options.hash = options.hash || "sha1"; + options.mgf = options.mgf || RSA.OAEP.$$eme_oaep_mgf1; + options.sLen = options.sLen || 20; + + var hLen = RSA.$$digestLength[options.hash]; + var emLen = Math.ceil(emBits / 8); + + if(emLen < hLen + options.sLen + 2) + throw new Error("Output length passed to emBits("+emBits+") is too small for the options specified("+options.hash+", "+options.sLen+"). To fix this issue increase the value of emBits. (minimum size: "+(8*hLen + 8*options.sLen + 9)+")") + + var mHash = crypt.createHash(options.hash); + mHash.end(M); + mHash = mHash.read(); + + var salt = crypt.randomBytes(options.sLen); + + var Mapostrophe = new Buffer(8 + hLen + options.sLen); + Mapostrophe.fill(0, 0, 8); + mHash.copy(Mapostrophe, 8); + salt.copy(Mapostrophe, 8+mHash.length); + + var H = crypt.createHash(options.hash); + H.end(Mapostrophe); + H = H.read(); + + 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] = 1; + salt.copy(DB, PS.length + 1); + + var dbMask = options.mgf(H, DB.length, options.hash); + + // XOR DB and dbMask together + for(var i = 0; ihash [String] Hash function to use when encoding. 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) + * + * @returns {Boolean} True if signature(EM) matches message(M) + */ + PSS.$$emsa_pss_verify = function(M, EM, emBits, options){ + if(!options) options = {}; + options.hash = options.hash || "sha1"; + options.mgf = options.mgf || RSA.OAEP.$$eme_oaep_mgf1; + options.sLen = options.sLen || 20; + + var hLen = RSA.$$digestLength[options.hash]; + var emLen = Math.ceil(emBits / 8); + + if(emLen < hLen + options.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; ihash [String] The hashing algorithm to use when signing and verifying. + * + * @param {Object} options + * @returns {RSA.PKCS1} + */ + var PKCS1 = function(options){ + if(!options) options = {}; + this.options = options; + }; + + PKCS1.prototype.maxMessageLength = function(key){ + return key.encryptedDataLength - 11; + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-7.2.1 + * + * @param {RSA.Key} key + * @param {Buffer} data + * @returns {Buffer} Encrypted Data + */ + PKCS1.prototype.encrypt = function(key, data){ + if(data.length > key.encryptedDataLength - 11) + throw new Error("Data is too long to be encrypted."); + + var em = PKCS1.$$eme_pkcs1_encode(data, key.encryptedDataLength); + em = new BigInteger(em); + em = key.$doPublic(em); + em = em.toBuffer(key.encryptedDataLength); + return em; + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-7.2.2 + * + * @param {RSA.Key} key + * @param {Buffer} data + * @returns {Buffer} Decrypted Data + */ + PKCS1.prototype.decrypt = function(key, data){ + if(data.length != key.encryptedDataLength) + throw new Error("Data is not of the right length to be decrypted by the given key") + + data = new BigInteger(data); + data = key.$doPrivate(data); + data = data.toBuffer(key.encryptedDataLength); + + return PKCS1.$$eme_pkcs1_decode(data); + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-8.2.1 + * + * @param {RSA.Key} key + * @param {Buffer} data + * @returns {Buffer} Signature + */ + PKCS1.prototype.sign = function(key, data){ + var S = PKCS1.$$emsa_pkcs1_v1_5(data, key.encryptedDataLength, this.options.hash); + S = new BigInteger(S); + S = key.$doPrivate(S); + return S.toBuffer(key.encryptedDataLength); + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-8.2.2 + * + * @param {RSA.Key} key + * @param {Buffer} data + * @param {Buffer} signature + * @returns {Boolean} Whether or not the signature is valid for given data. + */ + PKCS1.prototype.verify = function(key, data, signature){ + if(signature.length != key.encryptedDataLength) + return false; + + signature = new BigInteger(signature); + signature = key.$doPublic(signature); + signature = signature.toBuffer(key.encryptedDataLength); + var S = PKCS1.$$emsa_pkcs1_v1_5(data, key.encryptedDataLength, this.options.hash); + + return signature.toString("hex") == S.toString("hex"); + }; + + + + + + PKCS1.$$SIGNINFOHEAD = { + md2: new Buffer('3020300c06082a864886f70d020205000410', 'hex'), + md5: new Buffer('3020300c06082a864886f70d020505000410', 'hex'), + sha1: new Buffer('3021300906052b0e03021a05000414', 'hex'), + sha256: new Buffer('3031300d060960864801650304020105000420', 'hex'), + sha384: new Buffer('3041300d060960864801650304020205000430', 'hex'), + sha512: new Buffer('3051300d060960864801650304020305000440', 'hex') + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-9.2 + * + * @param {Buffer} M Message to encode. + * @param {uint} emLen Length of the output encoded message in bytes + * @param {String} hashFunction The hashing function to use when encoding (Default: "sha1")(SHA-1 or SHA-256/384/512 are recommended for new applications) + * @returns {Buffer} The encoded message. + */ + PKCS1.$$emsa_pkcs1_v1_5 = function(M, emLen, hashFunction){ + hashFunction = hashFunction || "sha1"; + + var H = crypt.createHash(hashFunction); + H.end(M); + H = H.read(); + + var tLen = PKCS1.$$SIGNINFOHEAD[hashFunction].length + H.length; + + if(emLen < tLen + 11) + throw new Error("The size of the output message (passed as emLen("+emLen+")) is too small to contain the signature with given hashing function. Minimum emLen with given parameters would be "+(tLen + 11)); + + var PS = new Buffer(emLen - tLen); + PS.fill(0xFF, 2); + PS[0] = 0; + PS[1] = 1; + PS[PS.length-1] = 0; + + return Buffer.concat([PS, PKCS1.$$SIGNINFOHEAD[hashFunction], H]); + }; + + /* + * https://tools.ietf.org/html/rfc3447#section-7.2.1 + * + * @param {Buffer} M + * @param {uint} emLen + * @returns {Buffer} + */ + PKCS1.$$eme_pkcs1_encode = function(M, emLen){ + var PS = crypt.randomBytes(emLen - M.length); + for(var i = 0; i 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); - } + for(var scheme in schemeEncrypting){ + (function(scheme) { + var encrypted = {}; + var decrypted = {}; + for(var i in dataBundle) { + (function(i) { + var key = null; + var suit = dataBundle[i]; + + it("should encrypt " + i + " using " + scheme, function () { + console.log(); + key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length]; + key.schemeEncryption = schemeEncrypting[scheme]; + encrypted[i] = key.encrypt(suit.data); + assert(Buffer.isBuffer(encrypted[i])); + assert(encrypted[i].length > 0); + }); + + it("should decrypt " + i + " using " + scheme, function () { + console.log(); + 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); + } + })(scheme); + } }); describe("Bad cases", function () { @@ -255,80 +268,83 @@ describe("NodeRSA", function(){ 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); - }); - - it("should verify " + i, function () { - assert(key.verify(suit.data, signed[i])); - }); - })(i); - } - - 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]); - } - - }); - - 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)); - }); - }); - })(environments[env]); - } - - describe("Compatibility of different environments", function () { + describe("Good cases", function () { + for(var scheme in schemeSigning){ + (function(scheme){ + for (var alg in signAlgorithms) { + (function (alg) { + var signed = {}; + var key = null; + + for (var i in dataBundle) { + (function (i) { + var suit = dataBundle[i]; + it("should sign " + i + " using " + scheme + " with hashing algorithm " + alg, function () { + //key = new NodeRSA(generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].getPrivatePEM()); + key = new NodeRSA(generatedKeys[5].getPrivatePEM()); // Some signatures+hash combinations will be larger than some of the smaller keys being tested. When that happens the signing process throws an error. + key.schemeSignature = schemeSigning[scheme]; + key.schemeSignature.options.hash = alg; + signed[i] = key.sign(suit.data); + assert(Buffer.isBuffer(signed[i])); + assert(signed[i].length > 0); + }); + + it("should verify " + i + " using " + scheme + " with hashing algorithm " + alg, function () { + assert(key.verify(suit.data, signed[i])); + }); + })(i); + } + })(signAlgorithms[alg]); + } + })(scheme); + } + }); + + + describe("Bad cases", function () { + it("incorrect data for verifying", function () { + var key = new NodeRSA(generatedKeys[5].getPrivatePEM()); + key.schemeSignature.options.hash = "sha1"; + var signed = key.sign('data1'); + assert(!key.verify('data2', signed)); + }); + + it("incorrect key for signing", function () { + var key = new NodeRSA(generatedKeys[5].getPublicPEM()); + key.schemeSignature.options.hash = "sha1"; + assert.throw(function () { + key.sign('data'); + }, Error, "It is not private key"); + }); + + it("incorrect key for verifying", function () { + var key1 = new NodeRSA(generatedKeys[5].getPrivatePEM()); + var key2 = new NodeRSA(generatedKeys[1].getPublicPEM()); + var signed = key1.sign('data'); + assert(!key2.verify('data', signed)); + }); + + it("incorrect key for verifying (empty)", function () { + var key = new NodeRSA(null); + key.schemeSignature.options.hash = "sha1"; + + assert.throw(function () { + key.verify('data', 'somesignature'); + }, Error, "It is not public key"); + }); + + it("different algorithms", function () { + var signKey = new NodeRSA(generatedKeys[0].getPrivatePEM()); + var verifyKey = new NodeRSA(generatedKeys[0].getPrivatePEM()); + NodeRSA.RSA.PSS.Default.options.hash = "sha1"; + var pssMD5 = new NodeRSA.RSA.PSS({hash: "md5"}); + verifyKey.schemeSignature = pssMD5; + var signed = signKey.sign('data'); + assert(!verifyKey.verify('data', signed)); + }); + }); + + describe.skip("Compatibility of different environments", function () { // This should no longer apply as both node and browser environments are using the rsa.js library for (var alg in signAlgorithms) { (function (alg) { it("signing with custom algorithm (" + alg + ")", function () { From 2c2ce4761667bfb97bef2967dc76b1025a323bf0 Mon Sep 17 00:00:00 2001 From: BAM5 Date: Tue, 18 Nov 2014 18:34:50 -0500 Subject: [PATCH 03/11] Added changelog --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4089b3e..9bde89d 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,12 @@ Questions, comments, bug reports, and pull requests are all welcome. ## Changelog +### 0.1.55 + * **The default schemes used to encrypt and sign data have changed from PKCS1 to the recommended OAEP and PSS standards** + * Overhauled the rsa.js library to allow for schemes and to allow for easy addition of schemes in the future and custom schemes. + * Modified NodeRSA functions to work with new rsa.js file. + * All changes should be backwards compatible + ### 0.1.54 * Added support for loading PEM key from Buffer (`fs.readFileSync()` output) * Added `isEmpty()` method From 15386632f4e90ac5d44fe45a162f9f7da3b2417f Mon Sep 17 00:00:00 2001 From: BAM5 Date: Tue, 18 Nov 2014 18:38:11 -0500 Subject: [PATCH 04/11] Major overhaul to add schemes. Everything should be backwards compatible. --- src/libs/rsa.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/rsa.js b/src/libs/rsa.js index 3bb4ac6..80bfbf1 100644 --- a/src/libs/rsa.js +++ b/src/libs/rsa.js @@ -44,10 +44,15 @@ * * Modified Key object to accept a padding key in it's options object. * padding key's value may be any object with the appropriate properties & method signatures: + * Encryption Schemes * maxMessageLength(key:RSA.Key):uint * encrypt(key:RSA.Key, message:Buffer):Buffer * decrypt(key:RSA.Key, encryptedMessage:Buffer):Buffer - * sign(key:RSA.key, data:Buffer, hash:String):Buffer + * + * Signature Schemes + * sign(key:RSA.Key, data:Buffer):Buffer + * verify(key:RSA.Key, data:Buffer, signature:Buffer):Boolean + * * If a padding scheme has options then the padding value can be an instantiated class. * This allows for future changes by making it easy to add new schemes. * This also allows for users to implement their own padding schemes, although not recommended. From f7627378cd207093c516c12cf0c2c9f43440dc72 Mon Sep 17 00:00:00 2001 From: BAM5 Date: Tue, 18 Nov 2014 19:40:26 -0500 Subject: [PATCH 05/11] Fixed error in example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9bde89d..b1e86ad 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Return max data size for encrypt in bytes. PKCS1 is still available with the following configuring* ```javascript -key.schemeEncryption = new NodeRSA.RSA.PKCS1.Default; +key.schemeEncryption = NodeRSA.RSA.PKCS1.Default; ``` ```javascript @@ -144,7 +144,7 @@ Return decrypted data.
PKCS1 is still available with the following configuring* ```javascript -key.schemeSignature = new NodeRSA.RSA.PKCS1.Default; +key.schemeSignature = NodeRSA.RSA.PKCS1.Default; ``` ```javascript From 6ddb480cb7bff9fb95309143a5750cf329835dc3 Mon Sep 17 00:00:00 2001 From: BAM5 Date: Tue, 18 Nov 2014 19:48:13 -0500 Subject: [PATCH 06/11] Fixed some comments referring to old code. --- src/libs/rsa.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/rsa.js b/src/libs/rsa.js index 80bfbf1..1ee6b6a 100644 --- a/src/libs/rsa.js +++ b/src/libs/rsa.js @@ -42,8 +42,8 @@ /* * OAEP / Padding function modifications * - * Modified Key object to accept a padding key in it's options object. - * padding key's value may be any object with the appropriate properties & method signatures: + * Modified Key object to utilize a scheme object when encrypting/decrypting and signing/verifying. + * The Scheme object may be any object with the appropriate method signatures: * Encryption Schemes * maxMessageLength(key:RSA.Key):uint * encrypt(key:RSA.Key, message:Buffer):Buffer @@ -53,9 +53,9 @@ * sign(key:RSA.Key, data:Buffer):Buffer * verify(key:RSA.Key, data:Buffer, signature:Buffer):Boolean * - * If a padding scheme has options then the padding value can be an instantiated class. - * This allows for future changes by making it easy to add new schemes. - * This also allows for users to implement their own padding schemes, although not recommended. + * If a scheme has options(like hashing function or mask generation function) then the scheme object can be an instantiated class. + * These modifications allow for future changes by making it easy to add new schemes. + * This also allows for users to implement their own schemes, although not recommended. * * 2014 BAM5 */ From 4f7393aa5973393e951f6bfaa691d6a30cfc5b87 Mon Sep 17 00:00:00 2001 From: rzcoder Date: Mon, 24 Nov 2014 02:04:07 +0500 Subject: [PATCH 07/11] Merge branch 'BAM5-master' Fixes bugs --- .gitignore | 1 + README.md | 93 +++-- gruntfile.js | 12 +- package.json | 7 +- src/NodeRSA.js | 258 ++++++++----- src/libs/jsbn.js | 41 +- src/libs/rsa.js | 835 +++++------------------------------------ src/schemes/oaep.js | 180 +++++++++ src/schemes/pkcs1.js | 175 +++++++++ src/schemes/pss.js | 187 +++++++++ src/schemes/schemes.js | 23 ++ src/utils.js | 6 +- test/tests.js | 465 ++++++++++++++--------- 13 files changed, 1168 insertions(+), 1115 deletions(-) create mode 100644 src/schemes/oaep.js create mode 100644 src/schemes/pkcs1.js create mode 100644 src/schemes/pss.js create mode 100644 src/schemes/schemes.js 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 b1e86ad..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,19 +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 + +#### 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 + } +} +``` -#### "Empty" key +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'+ @@ -116,13 +145,6 @@ Return max data size for encrypt in bytes. ### Encrypting/decrypting -*As of v0.1.55 the default encryption scheme is RSAES-OAEP using sha1 and mgf1. -PKCS1 is still available with the following configuring* - -```javascript -key.schemeEncryption = NodeRSA.RSA.PKCS1.Default; -``` - ```javascript key.encrypt(buffer, [encoding], [source_encoding]); ``` @@ -139,14 +161,6 @@ Return decrypted data.
**encoding** - encoding for result string. Can also take `'buffer'` for raw Buffer object, or `'json'` for automatic JSON.parse result. Default `'buffer'`. ### Signing/Verifying - -*As of v0.1.55 the default signature scheme is RSASSA-PSS using sha1. -PKCS1 is still available with the following configuring* - -```javascript -key.schemeSignature = NodeRSA.RSA.PKCS1.Default; -``` - ```javascript key.sign(buffer, [encoding], [source_encoding]); ``` @@ -161,44 +175,21 @@ Return result of check, `true` or `false`.
**source_encoding** - same as for `encrypt` method.
**signature_encoding** - encoding of given signature. May be `'buffer'`, `'binary'`, `'hex'` or `'base64'`. Default `'buffer'`. -### Changing Encryption/Signature schemes. - -Schemes are the way RSA packs up it's data before encrypting/decrypting/signing/verifying and there are a few ways that it can do it. The most common are RSAES-OAEP, RSASSA-PSS, RSAES-PKCS1-v1_5(encryption/decryption), and RSASSA-PKCS1-v1_5(signing/verifying). -As of v0.1.55 these 4 mentioned schemes are included in the package with the ability to easily add more later and by users. See below for how to configure NodeRSA to use these different schemes. - -*Note: The default encryption / signature schemes have been changed from PKCS1 to the more secure OAEP(encryption) and PSS(signature) schemes as per the recommendations of the spec, this can be breaking.* - -```javascript -key.schemeSignature = NodeRSA.RSA.PSS.Default; // This is an object that has been automatically instantiated and has the default settings for PSS signing. -key.schemeSignature = NodeRSA.RSA.PKCS1.Default; // This is an object that has been automatically instantiated and has the default settings for PKCS1 signing/encrypting. - -key.schemeEncryption = NodeRSA.RSA.OAEP.Default; // This is an object that has been automatically instantiated and has the default settings for OAEP encrypting. -key.schemeEncryption = NodeRSA.RSA.PKCS1.Default; // This is an object that has been automatically instantiated and has the default settings for PKCS1 signing/encrypting. -``` -To change schemes between the various defaults, each provided scheme class has a property named "Default" that provides a scheme object with the default settings for that scheme. - -```javascript -var pssMD5Scheme = new NodeRSA.RSA.PSS({ - // The options a scheme accepts should be documented in the scheme definition. - hash: "md5", // Use a different type of hashing function instead of sha1 - mgf: customMaskGenerationFunction // Use a custom mask generation function that accepts 3 parameters (seed, maskLength, hashFunction) -}); -key.schemeSignature = pssMD5Scheme; -key2.schemeSignature = pssMD5Scheme; -``` -Schemes can also be initialized with custom options and can be shared between keys. - ## Contributing Questions, comments, bug reports, and pull requests are all welcome. ## Changelog -### 0.1.55 - * **The default schemes used to encrypt and sign data have changed from PKCS1 to the recommended OAEP and PSS standards** - * Overhauled the rsa.js library to allow for schemes and to allow for easy addition of schemes in the future and custom schemes. - * Modified NodeRSA functions to work with new rsa.js file. - * All changes should be backwards compatible +### 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) 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 3c4a385..d672a86 100644 --- a/src/NodeRSA.js +++ b/src/NodeRSA.js @@ -8,33 +8,49 @@ */ var rsa = require('./libs/rsa.js'); +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 = {}; - - if(options){ - console.warn("There are no more options and the parameter is deprecated. It may be removed in the future."); - if(options.signingAlgorithm){ - console.warn("options.signingAlgorithm has been removed. In order to change the signature hashing algorithm create a signature scheme with the appropriate options and set NodeRSA.schemeSignature to that object. Schemes are defined in src/libs/rsa.js Default is RSASSA-PSS (RSA.PSS) with the hashing function sha1"); - this.schemeSignature = new rsa.PKCS1({hash: options.signingAlgorithm}); - } - if(options.environment) - console.warn("options.environment has been removed since all encryption is now done through the rsa.js library"); - } if (Buffer.isBuffer(key) || _.isString(key)) { this.loadFromPEM(key); @@ -42,8 +58,73 @@ module.exports = (function() { this.generateKeyPair(key.b, key.e); } } - - NodeRSA.RSA = rsa; + + /** + * 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 @@ -52,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; @@ -69,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'); } @@ -77,9 +158,9 @@ module.exports = (function() { if (/^\s*-----BEGIN RSA PRIVATE KEY-----\s*([A-Za-z0-9+/=]+\s*)+-----END RSA PRIVATE KEY-----\s*$/g.test(pem)) { this.$loadFromPrivatePEM(pem, 'base64'); } else if (/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=]+\s*)+-----END PUBLIC KEY-----\s*$/g.test(pem)) { - this.$loadFromPublicPKCS8(pem, 'base64'); + this.$loadFromPublicPEM(pem, 'base64'); } else - throw Error('Invalid key format'); + throw Error('Invalid PEM format'); this.$recalculateCache(); }; @@ -89,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')); @@ -116,10 +197,10 @@ module.exports = (function() { * * @param publicPEM {string} */ - NodeRSA.prototype.$loadFromPublicPKCS8 = 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')); @@ -141,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; }; @@ -149,36 +230,17 @@ 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); }; - /* - * In order to use a different encryption/signing/padding scheme then the default (OAEP for encrypting and PSS for signing) - * create a new scheme object with desired options and set the appropriate variable to that scheme object. - * Scheme classes are located in rsa.js and new ones can be easily added. - */ - Object.defineProperties(NodeRSA.prototype, { - schemeEncryption: { - enumerable: true, - get: function(){ return this.keyPair.schemeEncryption; }, - set: function(scheme){ this.keyPair.schemeEncryption = scheme; } - }, - - schemeSignature: { - enumerable: true, - get: function(){ return this.keyPair.schemeSignature; }, - set: function(scheme){ this.keyPair.schemeSignature = scheme; } - } - }); - /** * Encrypting data method * @@ -187,39 +249,38 @@ 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){ - buffer = this.$getDataForEcrypt(buffer, source_encoding); - try{ - var res = this.keyPair.encrypt(buffer); - } catch(e){ - console.warn(e.message); - return null; - } - - if (encoding == 'buffer' || !encoding) { - return res; - } else { - return res.toString(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); + } + } catch (e) { + throw Error('Error during encryption. Original error: ' + e); } }; /** * Decrypting data method * - * @param buffer {Buffer} - buffer for decrypting, or base64 string - * @param encoding - encoding for result string, can also take 'json' or 'buffer' for the automatic conversion of this type. Default "utf8" - * @returns {string|Buffer|object} + * @param buffer {Buffer} - buffer for decrypting + * @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; - var clone = new Buffer(buffer.length); - buffer.copy(clone); - try{ - return this.$getDecryptedData(this.keyPair.decrypt(clone), encoding); - } catch(e){ - console.warn(e.message); - return null; - } + 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); + } }; /** @@ -230,21 +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){ - if(!this.isPrivate()) throw Error("It is not private key"); - - // Move all the lifting to rsa.js module due to its wider options. - try{ - var res = this.keyPair.sign(this.$getDataForEcrypt(buffer, source_encoding)); - } catch(e){ - console.warn(e.message); - return null; - } - - if(encoding && encoding != 'buffer') - return res.toString(encoding); - - return res; + 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 (encoding && encoding != 'buffer') { + res = res.toString(encoding); + } + return res; }; /** @@ -256,23 +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){ - if(!this.isPublic()) throw Error("It is not public key"); - - signature_encoding = (!signature_encoding || signature_encoding == 'buffer' ? null : signature_encoding); - try{ - return this.keyPair.verify(this.$getDataForEcrypt(buffer, source_encoding), signature, signature_encoding); - } catch(e){ - console.warn(e.message); - return false; - } + 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); + 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; }; @@ -280,7 +331,6 @@ module.exports = (function() { if (!this.isPublic()) { throw Error("It is not public key"); } - return this.$cache.publicPEM; }; @@ -299,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)) { @@ -317,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') { @@ -333,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(); }; @@ -342,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; } @@ -379,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 eb92939..3bca543 100644 --- a/src/libs/jsbn.js +++ b/src/libs/jsbn.js @@ -34,12 +34,10 @@ /* * Added Node.js Buffers support * 2014 rzcoder - * - * Extended bnToBuffer to allow length specifying. - * 2014 BAM5 */ var crypt = require('crypto'); +var _ = require('lodash'); // Bits per digit var dbits; @@ -815,26 +813,29 @@ function bnToByteArray() { /** * return Buffer object - * @param trimOrSize {boolean | uint} slice buffer if first element == 0 + * @param trim {boolean} slice buffer if first element == 0 * @returns {Buffer} */ function bnToBuffer(trimOrSize) { - if(trimOrSize === true || trimOrSize === undefined){ - var res = new Buffer(this.toByteArray()); - return trimOrSize && res[0] === 0 ? res.slice(1) : res; - } else{ - var res = new Buffer(this.toByteArray()); - if(res.length > trimOrSize){ - for(var i=0; i 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 1ee6b6a..245bb29 100644 --- a/src/libs/rsa.js +++ b/src/libs/rsa.js @@ -39,45 +39,14 @@ * 2014 rzcoder */ -/* - * OAEP / Padding function modifications - * - * Modified Key object to utilize a scheme object when encrypting/decrypting and signing/verifying. - * The Scheme object may be any object with the appropriate method signatures: - * Encryption Schemes - * maxMessageLength(key:RSA.Key):uint - * encrypt(key:RSA.Key, message:Buffer):Buffer - * decrypt(key:RSA.Key, encryptedMessage:Buffer):Buffer - * - * Signature Schemes - * sign(key:RSA.Key, data:Buffer):Buffer - * verify(key:RSA.Key, data:Buffer, signature:Buffer):Boolean - * - * If a scheme has options(like hashing function or mask generation function) then the scheme object can be an instantiated class. - * These modifications allow for future changes by making it easy to add new schemes. - * This also allows for users to implement their own schemes, although not recommended. - * - * 2014 BAM5 - */ - +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 RSA = module.exports; - -RSA.$$digestLength = { // In Bytes - "md2": 16, - "md5": 16, - "sha1": 20, - "sha256": 32, - "sha384": 48, - "sha512": 64 -}; +var schemes = require('../schemes/schemes.js'); -RSA.BigInteger = BigInteger; -RSA.Key = (function() { +exports.BigInteger = BigInteger; +module.exports.Key = (function() { /** * RSA key constructor * @@ -99,15 +68,20 @@ RSA.Key = (function() { this.dmp1 = null; this.dmq1 = null; this.coeff = null; - - this.usedSign = false; - this.usedDecrypt = false; - - // Default schemes as per recommendation by specification - this.schemeEncryption = RSA.OAEP.Default; - this.schemeSignature = RSA.PSS.Default; } + 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 @@ -202,15 +176,17 @@ RSA.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); }; @@ -226,100 +202,82 @@ RSA.Key = (function() { }; /** - * Return the encryption of buffer with the given scheme, if no scheme is given then this.schemeEncryption is used. + * Return the PKCS#1 RSA encryption of buffer * @param buffer {Buffer} - * @param scheme {Object} * @returns {Buffer} */ - RSAKey.prototype.encrypt = function (buffer, scheme){ - scheme = scheme || this.schemeEncryption; + RSAKey.prototype.encrypt = function (buffer) { + var buffers = []; var results = []; - var i, encrypted, temp; - var chunks = new Array(Math.ceil(buffer.length / scheme.maxMessageLength(this)) || 1); - - if(chunks.length == 1) - chunks[0] = buffer; - else{ - // Message is too long to be encrypted. Cut message up into smaller encryptable message chunks. - var chunkSize = Math.ceil(buffer.length / chunks.length || 1); // each buffer size - for(i = 0; i this.encryptedDataLength) throw new Error("The returned encrypted message is too long. This is most likely an issue with the scheme that is being used."); - results.push(encrypted); + 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 + + if ( buffersCount == 1) { + buffers.push(buffer); + } else { + for (var bufNum = 0; bufNum < buffersCount; bufNum++) { + buffers.push(buffer.slice(bufNum * dividedSize, (bufNum+1) * dividedSize)); + } + } + + for(var i in buffers) { + var buf = buffers[i]; + + var m = new BigInteger(this.encryptionScheme.encPad(buf)); + var c = this.$doPublic(m); + + if (c === null) { + return null; + } + + 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); } return Buffer.concat(results); }; /** - * Return the decryption of buffer that uses the given scheme, if scheme is not set then this.schemeEncryption is presumed to be the scheme used in this message. - * @param buffer {Buffer} This buffer may get mangled to save memory depending on the scheme function used. + * Return the PKCS#1 RSA decryption of buffer + * @param buffer {Buffer} * @returns {Buffer} */ - RSAKey.prototype.decrypt = function (buffer, scheme){ - if(this.usedSign) console.warn("It is against recommendations to use the same private key for encryption and signatures.") - this.usedDecrypt = true; - - if(buffer.length % this.encryptedDataLength !== 0) - throw Error("Incorrect data or key, the buffer must have a length that is the multiple of this key's length"); - - scheme = scheme || this.schemeEncryption; + RSAKey.prototype.decrypt = function (buffer) { + if (buffer.length % this.encryptedDataLength > 0) { + throw Error('Incorrect data or key'); + } var result = []; - var chunkCount = buffer.length / this.encryptedDataLength; + var offset = 0; + var length = 0; + var buffersCount = buffer.length / this.encryptedDataLength; + + for (var i = 0; i < buffersCount; i++) { + offset = i * this.encryptedDataLength; + length = offset + this.encryptedDataLength; - for(var i = 0; i < chunkCount; i++) - result.push(scheme.decrypt( - this, // Key - buffer.slice( // Cut out a chunk of the buffer to decrypt with Key - i*this.encryptedDataLength, // Beginning of chunk - (i+1)*this.encryptedDataLength // End of chunk - ) - )); + var c = new BigInteger(buffer.slice(offset, Math.min(length, buffer.length))); + var m = this.$doPrivate(c); + result.push(this.encryptionScheme.encUnPad(m.toBuffer(this.encryptedDataLength))); + } return Buffer.concat(result); }; - - - RSAKey.prototype.sign = function (data, hashAlgorithm) { - if(this.usedDecrypt) console.warn("It is against recommendations to use the same private key for encryption and signatures.") - if(this.hashAlgorithm){ - console.warn("! hashAlgorithm parameter is deprecated and may be removed in future versions."); - if(!RSA.PKCS1.Temp) RSA.PKCS1.Temp = new RSA.PKCS1(); - RSA.PKCS1.Temp.options.hash = hashAlgorithm; - var signature = RSA.PKCS1.Temp.sign(this, data); - } else - var signature = this.schemeSignature.sign(this, data); - - this.usedSign = true; - return signature; + 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); - - if(hashAlgorithm){ - console.warn("! hashAlgorithm parameter is deprecated and may be removed in future versions."); - if(!RSA.PKCS1.Temp) RSA.PKCS1.Temp = new RSA.PKCS1(); - RSA.PKCS1.Temp.options.hash = hashAlgorithm; - return RSA.PKCS1.Temp.verify(this, buffer, signature); - } - - return this.schemeSignature.verify(this, buffer, signature); + RSAKey.prototype.verify = function (buffer, signature, signature_encoding) { + return this.signingScheme.verify.apply(this.signingScheme, arguments); }; - - + Object.defineProperty(RSAKey.prototype, 'keySize', { get: function() { return this.cache.keyBitLength; } }); @@ -329,648 +287,23 @@ RSA.Key = (function() { }); Object.defineProperty(RSAKey.prototype, 'maxMessageLength', { - get: function() { return this.schemeEncryption.maxMessageLength(this); } + get: function() { return this.encryptionScheme.maxMessageLength(); } }); - Object.defineProperties(RSAKey.prototype, { - schemeEncryption: { - enumerable: true, - get: function(){ return this._schemeEncryption; }, - set: function(scheme){ - if(RSA.isEncryptionScheme(scheme)) this._schemeEncryption = scheme; - else throw new Error("Provided object is not an encryption scheme"); - } - }, - - schemeSignature: { - enumerable: true, - get: function(){ return this._schemeSignature; }, - set: function(scheme){ - if(RSA.isSignatureScheme(scheme)) this._schemeSignature = scheme; - else throw new Error("Provided object is not a signature scheme"); - } - } - }); - /** * Caching key data */ RSAKey.prototype.$$recalculateCache = function () { - this.cache = this.cache || {}; - // Bit & byte length - this.cache.keyBitLength = this.n.bitLength(); - this.cache.keyBitLength += this.cache.keyBitLength % 2; - this.cache.keyByteLength = (this.cache.keyBitLength + 6) >> 3; - }; + this.cache = this.cache || {}; + // Bit & byte length + this.cache.keyBitLength = this.n.bitLength(); + if (this.cache.keyBitLength % 2 == 1) { + this.cache.keyBitLength = this.cache.keyBitLength + 1; + } + this.cache.keyByteLength = (this.cache.keyBitLength + 6) >> 3; + }; return RSAKey; })(); - - - - -RSA.isEncryptionScheme = function(object){ - return ("maxMessageLength" in object && "encrypt" in object && "decrypt" in object); -}; - -RSA.isSignatureScheme = function(object){ - return ("sign" in object && "verify" in object); -}; - -RSA.OAEP = (function(){ - - // OAEP Padding Scheme - - /* - * Retuns an object that OAEP pads, encodes, and encrypts buffer objects with the options specified in options. - * - * options [Object] Options for the encoding (The defaults should be used unless in special use cases and user knows what they're doing) - * ├>label [Buffer] Value to pass to the $$eme_oaep_encode and $$eme_oaep_decode functions as the L parameter. - * ├>hash [String] The hashing function to use when encoding and creating checksums (Default: "sha1")(only SHA-1 and SHA-256/384/512 are recommended)(L parameter max size depends on hashing function, however sha1 and sha256's size limit are too large to touch (TBytes) so length checking is NOT implemented) - * └>mgf [function] The mask generation function (Default: OAEP.$$eme_oaep_mgf1) - * - * - * @param {Object} options - * @returns {RSA.OAEP} - */ - var OAEP = function(options){ - if(!options) options = {}; - this.options = options; - this.count = 0; - }; - - OAEP.prototype.maxMessageLength = function(key){ - return key.encryptedDataLength - 2*RSA.$$digestLength[this.options.hash] - 2; - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-7.1.1 - * - * @param {RSA.Key} key - * @param {Buffer} message - * @returns {Buffer} - */ - OAEP.prototype.encrypt = function(key, message){ - var m = OAEP.$$eme_oaep_encode(message, this.options.label, key.encryptedDataLength, this.options); - m = new BigInteger(m); - m = key.$doPublic(m); - m = m.toBuffer(key.encryptedDataLength); - return m; - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-7.1.2 - * - * @param {RSA.Key} key - * @param {Buffer} encMessage - * @returns {Buffer} - */ - OAEP.prototype.decrypt = function(key, encMessage){ - if(encMessage.length != key.encryptedDataLength || encMessage.length < 2*RSA.$$digestLength[this.options.hash] + 2) - throw new Error("Decryption Error"); - - encMessage = new BigInteger(encMessage); - encMessage = key.$doPrivate(encMessage); - encMessage = encMessage.toBuffer(key.encryptedDataLength); - - return OAEP.$$eme_oaep_decode(encMessage, this.options.label, this.options); - }; - - - - - - /* - * 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 - */ - OAEP.$$eme_oaep_mgf1 = function(seed, maskLength, hashFunction){ - hashFunction = hashFunction || "sha1"; - var hLen = RSA.$$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) { - 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); - }; - - /* - * Encode message with OAEP format + padding - * - * https://tools.ietf.org/html/rfc3447#section-7.1.1 - * - * Parameters: - * M [Buffer] The message bytes to be encoded - * L [Buffer] Label to associate with this message. Usually left blank. (Default: '') - * emLen [int] Size of the returned encoded message - * options [Object] Options for the encoding (The defaults should be used unless in special use cases and user knows what they're doing) - * ├>hash [String] The hashing function to use when encoding and creating checksums (Default: "sha1") (L parameter max size depends on hashing function, however sha1 and sha256's size limit are too large to touch so length checking is NOT implemented) - * └>mgf [function] The mask generation function (Default: $$eme_oaep_mgf1) - */ - OAEP.$$eme_oaep_encode = function(M, L, emLen, options){ - // Prepare options - if(!options) options = {}; - options.hash = options.hash || "sha1"; - options.mgf = options.mgf || OAEP.$$eme_oaep_mgf1; - - var hLen = RSA.$$digestLength[options.hash]; - - // Make sure we can put message into an encoded message of emLen bytes - if(M.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)+")"); - - L = L || new Buffer(0); - var lHash = crypt.createHash(options.hash); - lHash.end(L); - lHash = lHash.read(); - - var PS = new Buffer(emLen - M.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, M]); - var seed = crypt.randomBytes(hLen); - - // mask = dbMask - var mask = options.mgf(seed, DB.length, options.hash); - // XOR DB and dbMask together. - for(var i = 0; ihash [String] The hashing function to use when decoding and checking checksums (Default: "sha1") (L parameter max size depends on hashing function, however sha1 and sha256's size limit are too large to touch so length checking is NOT implemented) - * └>mgf [function] The mask generation function (Default: $$eme_oaep_mgf1) - */ - OAEP.$$eme_oaep_decode = function(EM, L, options){ - // Prepare options - if(!options) options = {}; - options.hash = options.hash || "sha1"; - options.mgf = options.mgf || OAEP.$$eme_oaep_mgf1; - - var hLen = RSA.$$digestLength[options.hash]; - - // Check to see if EM is a properly encoded OAEP message - if(EM.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 = EM.slice(1, hLen+1); // seed = maskedSeed - var DB = EM.slice(1+hLen); // DB = maskedDB - - var mask = options.mgf(DB, hLen, options.hash); // seedMask - // XOR maskedSeed and seedMask together to get the original seed. - for(var i = 0; ihash [String] The hashing function to use when generating signatures (Default: "sha1")(only SHA-1 and SHA-256/384/512 are recommended) - * ├>mgf [function] The mask generation function (Default: RSA.OAEP.$$eme_oaep_mgf1) - * └>sLen [uint] The length of the salt to generate. (default = 20) - * - * @param {Object} options - * @returns {RSA.PSS} - */ - var PSS = function(options){ - if(!options) options = {}; - this.options = options; - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-8.1.1 - * - * @param {RSA.Key} key - * @param {Buffer} data - * @returns {Buffer} The generated signature - */ - PSS.prototype.sign = function(key, data){ - var encoded = PSS.$$emsa_pss_encode(data, key.keySize - 1, this.options); - encoded = new BigInteger(encoded); - return key.$doPrivate(encoded).toBuffer(key.encryptedDataLength); - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-8.1.2 - * - * @param {RSA.Key} key - * @param {Buffer} data - * @param {Buffer} signature - * @returns {Boolean} True if signature is valid, false otherwise. - */ - PSS.prototype.verify = function(key, data, signature){ - signature = new BigInteger(signature); - - var emLen = Math.ceil((key.keySize - 1)/8); - signature = key.$doPublic(signature).toBuffer(emLen); - - return PSS.$$emsa_pss_verify(data, signature, key.keySize -1, this.options); - }; - - - - - - /* - * 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) - * options [Object] An object that contains the following keys that specify certain options for encoding. - * ├>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) - * - * @returns {Buffer} The encoded message - */ - PSS.$$emsa_pss_encode = function(M, emBits, options){ - if(!options) options = {}; - options.hash = options.hash || "sha1"; - options.mgf = options.mgf || RSA.OAEP.$$eme_oaep_mgf1; - options.sLen = options.sLen || 20; - - var hLen = RSA.$$digestLength[options.hash]; - var emLen = Math.ceil(emBits / 8); - - if(emLen < hLen + options.sLen + 2) - throw new Error("Output length passed to emBits("+emBits+") is too small for the options specified("+options.hash+", "+options.sLen+"). To fix this issue increase the value of emBits. (minimum size: "+(8*hLen + 8*options.sLen + 9)+")") - - var mHash = crypt.createHash(options.hash); - mHash.end(M); - mHash = mHash.read(); - - var salt = crypt.randomBytes(options.sLen); - - var Mapostrophe = new Buffer(8 + hLen + options.sLen); - Mapostrophe.fill(0, 0, 8); - mHash.copy(Mapostrophe, 8); - salt.copy(Mapostrophe, 8+mHash.length); - - var H = crypt.createHash(options.hash); - H.end(Mapostrophe); - H = H.read(); - - 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] = 1; - salt.copy(DB, PS.length + 1); - - var dbMask = options.mgf(H, DB.length, options.hash); - - // XOR DB and dbMask together - for(var i = 0; ihash [String] Hash function to use when encoding. 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) - * - * @returns {Boolean} True if signature(EM) matches message(M) - */ - PSS.$$emsa_pss_verify = function(M, EM, emBits, options){ - if(!options) options = {}; - options.hash = options.hash || "sha1"; - options.mgf = options.mgf || RSA.OAEP.$$eme_oaep_mgf1; - options.sLen = options.sLen || 20; - - var hLen = RSA.$$digestLength[options.hash]; - var emLen = Math.ceil(emBits / 8); - - if(emLen < hLen + options.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; ihash [String] The hashing algorithm to use when signing and verifying. - * - * @param {Object} options - * @returns {RSA.PKCS1} - */ - var PKCS1 = function(options){ - if(!options) options = {}; - this.options = options; - }; - - PKCS1.prototype.maxMessageLength = function(key){ - return key.encryptedDataLength - 11; - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-7.2.1 - * - * @param {RSA.Key} key - * @param {Buffer} data - * @returns {Buffer} Encrypted Data - */ - PKCS1.prototype.encrypt = function(key, data){ - if(data.length > key.encryptedDataLength - 11) - throw new Error("Data is too long to be encrypted."); - - var em = PKCS1.$$eme_pkcs1_encode(data, key.encryptedDataLength); - em = new BigInteger(em); - em = key.$doPublic(em); - em = em.toBuffer(key.encryptedDataLength); - return em; - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-7.2.2 - * - * @param {RSA.Key} key - * @param {Buffer} data - * @returns {Buffer} Decrypted Data - */ - PKCS1.prototype.decrypt = function(key, data){ - if(data.length != key.encryptedDataLength) - throw new Error("Data is not of the right length to be decrypted by the given key") - - data = new BigInteger(data); - data = key.$doPrivate(data); - data = data.toBuffer(key.encryptedDataLength); - - return PKCS1.$$eme_pkcs1_decode(data); - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-8.2.1 - * - * @param {RSA.Key} key - * @param {Buffer} data - * @returns {Buffer} Signature - */ - PKCS1.prototype.sign = function(key, data){ - var S = PKCS1.$$emsa_pkcs1_v1_5(data, key.encryptedDataLength, this.options.hash); - S = new BigInteger(S); - S = key.$doPrivate(S); - return S.toBuffer(key.encryptedDataLength); - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-8.2.2 - * - * @param {RSA.Key} key - * @param {Buffer} data - * @param {Buffer} signature - * @returns {Boolean} Whether or not the signature is valid for given data. - */ - PKCS1.prototype.verify = function(key, data, signature){ - if(signature.length != key.encryptedDataLength) - return false; - - signature = new BigInteger(signature); - signature = key.$doPublic(signature); - signature = signature.toBuffer(key.encryptedDataLength); - var S = PKCS1.$$emsa_pkcs1_v1_5(data, key.encryptedDataLength, this.options.hash); - - return signature.toString("hex") == S.toString("hex"); - }; - - - - - - PKCS1.$$SIGNINFOHEAD = { - md2: new Buffer('3020300c06082a864886f70d020205000410', 'hex'), - md5: new Buffer('3020300c06082a864886f70d020505000410', 'hex'), - sha1: new Buffer('3021300906052b0e03021a05000414', 'hex'), - sha256: new Buffer('3031300d060960864801650304020105000420', 'hex'), - sha384: new Buffer('3041300d060960864801650304020205000430', 'hex'), - sha512: new Buffer('3051300d060960864801650304020305000440', 'hex') - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-9.2 - * - * @param {Buffer} M Message to encode. - * @param {uint} emLen Length of the output encoded message in bytes - * @param {String} hashFunction The hashing function to use when encoding (Default: "sha1")(SHA-1 or SHA-256/384/512 are recommended for new applications) - * @returns {Buffer} The encoded message. - */ - PKCS1.$$emsa_pkcs1_v1_5 = function(M, emLen, hashFunction){ - hashFunction = hashFunction || "sha1"; - - var H = crypt.createHash(hashFunction); - H.end(M); - H = H.read(); - - var tLen = PKCS1.$$SIGNINFOHEAD[hashFunction].length + H.length; - - if(emLen < tLen + 11) - throw new Error("The size of the output message (passed as emLen("+emLen+")) is too small to contain the signature with given hashing function. Minimum emLen with given parameters would be "+(tLen + 11)); - - var PS = new Buffer(emLen - tLen); - PS.fill(0xFF, 2); - PS[0] = 0; - PS[1] = 1; - PS[PS.length-1] = 0; - - return Buffer.concat([PS, PKCS1.$$SIGNINFOHEAD[hashFunction], H]); - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-7.2.1 - * - * @param {Buffer} M - * @param {uint} emLen - * @returns {Buffer} - */ - PKCS1.$$eme_pkcs1_encode = function(M, emLen){ - var PS = crypt.randomBytes(emLen - M.length); - for(var i = 0; i 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 0da6541..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,15 +17,13 @@ describe("NodeRSA", function(){ {b: 1024} // 'e' should be 65537 ]; - var signAlgorithms = ['md5', 'sha1', "sha256", "sha384", "sha512"]; - var schemeSigning = { - pss: NodeRSA.RSA.PSS.Default, - pkcs1: NodeRSA.RSA.PKCS1.Default - }; - var schemeEncrypting = { - oaep: NodeRSA.RSA.OAEP.Default, - pkcs1: NodeRSA.RSA.PKCS1.Default - }; + 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": { @@ -35,7 +32,7 @@ describe("NodeRSA", function(){ }, "unicode string": { data: "ascii + юникод スラ ⑨", - encoding: "utf8" + encoding: "utf8" }, "empty string": { data: "", @@ -63,33 +60,90 @@ 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[generatedKeys.length - 1].schemeEncryption = NodeRSA.RSA.PKCS1.Default; + 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); - //assert.equal(generatedKeys[generatedKeys.length - 1].getMaxMessageSize(), (size.b / 8 - 11)); // This one depends on the scheme the key uses + 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", function () { - var key = new NodeRSA(null); - assert.equal(key.isEmpty(), true); - }); - - it.skip("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(){ @@ -217,159 +271,216 @@ describe("NodeRSA", function(){ }); }); - describe("Encrypting & decrypting", function(){ - describe("Good cases", function () { - for(var scheme in schemeEncrypting){ - (function(scheme) { - var encrypted = {}; - var decrypted = {}; - for(var i in dataBundle) { - (function(i) { - var key = null; - var suit = dataBundle[i]; - - it("should encrypt " + i + " using " + scheme, function () { - console.log(); - key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length]; - key.schemeEncryption = schemeEncrypting[scheme]; - encrypted[i] = key.encrypt(suit.data); - assert(Buffer.isBuffer(encrypted[i])); - assert(encrypted[i].length > 0); - }); - - it("should decrypt " + i + " using " + scheme, function () { - console.log(); - 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); - } - })(scheme); - } - }); - - 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("Signing & verifying", function () { - describe("Good cases", function () { - for(var scheme in schemeSigning){ - (function(scheme){ - for (var alg in signAlgorithms) { - (function (alg) { - var signed = {}; - var key = null; - - for (var i in dataBundle) { - (function (i) { - var suit = dataBundle[i]; - it("should sign " + i + " using " + scheme + " with hashing algorithm " + alg, function () { - //key = new NodeRSA(generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].getPrivatePEM()); - key = new NodeRSA(generatedKeys[5].getPrivatePEM()); // Some signatures+hash combinations will be larger than some of the smaller keys being tested. When that happens the signing process throws an error. - key.schemeSignature = schemeSigning[scheme]; - key.schemeSignature.options.hash = alg; - signed[i] = key.sign(suit.data); - assert(Buffer.isBuffer(signed[i])); - assert(signed[i].length > 0); - }); - - it("should verify " + i + " using " + scheme + " with hashing algorithm " + alg, function () { - assert(key.verify(suit.data, signed[i])); - }); - })(i); - } - })(signAlgorithms[alg]); - } - })(scheme); - } - }); - - - describe("Bad cases", function () { - it("incorrect data for verifying", function () { - var key = new NodeRSA(generatedKeys[5].getPrivatePEM()); - key.schemeSignature.options.hash = "sha1"; - var signed = key.sign('data1'); - assert(!key.verify('data2', signed)); - }); - - it("incorrect key for signing", function () { - var key = new NodeRSA(generatedKeys[5].getPublicPEM()); - key.schemeSignature.options.hash = "sha1"; - assert.throw(function () { - key.sign('data'); - }, Error, "It is not private key"); - }); - - it("incorrect key for verifying", function () { - var key1 = new NodeRSA(generatedKeys[5].getPrivatePEM()); - var key2 = new NodeRSA(generatedKeys[1].getPublicPEM()); - var signed = key1.sign('data'); - assert(!key2.verify('data', signed)); - }); - - it("incorrect key for verifying (empty)", function () { - var key = new NodeRSA(null); - key.schemeSignature.options.hash = "sha1"; - - assert.throw(function () { - key.verify('data', 'somesignature'); - }, Error, "It is not public key"); - }); - - it("different algorithms", function () { - var signKey = new NodeRSA(generatedKeys[0].getPrivatePEM()); - var verifyKey = new NodeRSA(generatedKeys[0].getPrivatePEM()); - NodeRSA.RSA.PSS.Default.options.hash = "sha1"; - var pssMD5 = new NodeRSA.RSA.PSS({hash: "md5"}); - verifyKey.schemeSignature = pssMD5; - var signed = signKey.sign('data'); - assert(!verifyKey.verify('data', signed)); - }); - }); - - describe.skip("Compatibility of different environments", function () { // This should no longer apply as both node and browser environments are using the rsa.js library - 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')); + 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); + } }); - 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'))); + 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]); + } + }); - 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'))); + describe("Signing & verifying", function () { + 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]); + } + }); + + 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)); + }); + }); + })(envs[env]); + } + + if (scheme !== 'pkcs1') { + return; + } + + 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]); + } }); - })(signAlgorithms[alg]); - } - - }); - }); + }); + })(signingSchemes[scheme_i]); + } + });*/ }); \ No newline at end of file From b2569b6865f9a8c5d9ac2011ad3248e1c279f9c2 Mon Sep 17 00:00:00 2001 From: rzcoder Date: Mon, 24 Nov 2014 02:20:06 +0500 Subject: [PATCH 08/11] fixes tests --- test/tests.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/tests.js b/test/tests.js index cec4bbe..ead37c3 100644 --- a/test/tests.js +++ b/test/tests.js @@ -92,6 +92,19 @@ describe("NodeRSA", function(){ assert.equal(key.$options.signingSchemeOptions.hash, null); }); + it("change options", function () { + var key = new NodeRSA(null, {signingScheme: 'pss'}); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, null); + key.setOptions({signingScheme: 'pkcs1'}); + assert.equal(key.$options.signingScheme, 'pkcs1'); + assert.equal(key.$options.signingSchemeOptions.hash, null); + key.setOptions({signingScheme: 'pkcs1-sha256'}); + assert.equal(key.$options.signingScheme, 'pkcs1'); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha256'); + }); + + it("advanced options change", function () { var key = new NodeRSA(null); key.setOptions({ @@ -130,7 +143,7 @@ describe("NodeRSA", function(){ }); }); - /*describe("Work with keys", function() { + describe("Work with keys", function() { describe("Generating keys", function() { for (var size in keySizes) { (function (size) { @@ -482,5 +495,5 @@ describe("NodeRSA", function(){ }); })(signingSchemes[scheme_i]); } - });*/ + }); }); \ No newline at end of file From 7af7833acb628de639f2978fd2cbc47af6e616a3 Mon Sep 17 00:00:00 2001 From: rzcoder Date: Mon, 24 Nov 2014 02:22:38 +0500 Subject: [PATCH 09/11] fixes tests --- test/tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tests.js b/test/tests.js index ead37c3..053549e 100644 --- a/test/tests.js +++ b/test/tests.js @@ -93,9 +93,9 @@ describe("NodeRSA", function(){ }); it("change options", function () { - var key = new NodeRSA(null, {signingScheme: 'pss'}); + var key = new NodeRSA(null, {signingScheme: 'pss-sha1'}); assert.equal(key.$options.signingScheme, 'pss'); - assert.equal(key.$options.signingSchemeOptions.hash, null); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha1'); key.setOptions({signingScheme: 'pkcs1'}); assert.equal(key.$options.signingScheme, 'pkcs1'); assert.equal(key.$options.signingSchemeOptions.hash, null); From 1cdd7a9efbf44db7b1c0ce31782f208cd57d326c Mon Sep 17 00:00:00 2001 From: rzcoder Date: Mon, 24 Nov 2014 02:42:41 +0500 Subject: [PATCH 10/11] readme fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26c9025..59e9a18 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ var key = new NodeRSA([key], [options]); **options** - additional settings #### Options -You can specify some options when key create (by second constructor argument) or over `key.setOptions()` method. +You can specify some options 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'`. From 962721244edf49539ef61a4c7c5237b69b0a918e Mon Sep 17 00:00:00 2001 From: rzcoder Date: Mon, 24 Nov 2014 16:12:30 +0500 Subject: [PATCH 11/11] rename some method --- README.md | 12 ++++++---- src/NodeRSA.js | 8 +++---- src/schemes/pkcs1.js | 4 ++-- test/tests.js | 57 +++++++++++++++++++++++--------------------- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 59e9a18..c0bfa20 100644 --- a/README.md +++ b/README.md @@ -107,15 +107,15 @@ Also you can use next methods: ```javascript key.generateKeyPair([bits], [exp]); -key.loadFromPEM(pem_string|buffer_contains_pem); +key.importKey(pem_string|buffer_contains_pem); ``` **bits** - key size in bits. 2048 by default. **exp** - public exponent. 65537 by default. ### Export keys ```javascript -key.getPrivatePEM(); -key.getPublicPEM(); +key.exportPrivate(); +key.exportPublic(); ``` ### Properties @@ -182,15 +182,19 @@ Questions, comments, bug reports, and pull requests are all welcome. ## Changelog ### 0.2.0 + * **`.getPublicPEM()` method was renamed to `.exportPublic()`** + * **`.getPrivatePEM()` method was renamed to `.exportPrivate()`** + * **`.loadFromPEM()` method was renamed to `.importKey()`** * 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` + * **`options.signingAlgorithm` was renamed 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/src/NodeRSA.js b/src/NodeRSA.js index d672a86..97528e2 100644 --- a/src/NodeRSA.js +++ b/src/NodeRSA.js @@ -53,7 +53,7 @@ module.exports = (function () { this.$cache = {}; if (Buffer.isBuffer(key) || _.isString(key)) { - this.loadFromPEM(key); + this.importKey(key); } else if (_.isObject(key)) { this.generateKeyPair(key.b, key.e); } @@ -150,7 +150,7 @@ module.exports = (function () { * Load key from PEM string * @param pem {string} */ - NodeRSA.prototype.loadFromPEM = function (pem) { + NodeRSA.prototype.importKey = function (pem) { if (Buffer.isBuffer(pem)) { pem = pem.toString('utf8'); } @@ -320,14 +320,14 @@ module.exports = (function () { return this.keyPair.verify(this.$getDataForEcrypt(buffer, source_encoding), signature, signature_encoding); }; - NodeRSA.prototype.getPrivatePEM = function () { + NodeRSA.prototype.exportPrivate = function () { if (!this.isPrivate()) { throw Error("It is not private key"); } return this.$cache.privatePEM; }; - NodeRSA.prototype.getPublicPEM = function () { + NodeRSA.prototype.exportPublic = function () { if (!this.isPublic()) { throw Error("It is not public key"); } diff --git a/src/schemes/pkcs1.js b/src/schemes/pkcs1.js index efd86d2..d46e297 100644 --- a/src/schemes/pkcs1.js +++ b/src/schemes/pkcs1.js @@ -115,7 +115,7 @@ module.exports.makeScheme = function (key, options) { } else { var signer = crypt.createSign('RSA-' + hashAlgorithm.toUpperCase()); signer.update(buffer); - return signer.sign(this.options.rsaUtils.getPrivatePEM()); + return signer.sign(this.options.rsaUtils.exportPrivate()); } }; @@ -137,7 +137,7 @@ module.exports.makeScheme = function (key, options) { } else { var verifier = crypt.createVerify('RSA-' + hashAlgorithm.toUpperCase()); verifier.update(buffer); - return verifier.verify(this.options.rsaUtils.getPublicPEM(), signature, signature_encoding); + return verifier.verify(this.options.rsaUtils.exportPublic(), signature, signature_encoding); } }; diff --git a/test/tests.js b/test/tests.js index 053549e..90c1d76 100644 --- a/test/tests.js +++ b/test/tests.js @@ -242,43 +242,46 @@ describe("NodeRSA", function(){ assert(!publicNodeRSA.isPrivate()); }); - it(".getPrivatePEM() should return private PEM string", function(){ - assert.equal(privateNodeRSA.getPrivatePEM(), privateKeyPEM); + it(".exportPrivate() should return private PEM string", function(){ + assert.equal(privateNodeRSA.exportPrivate(), privateKeyPEM); }); - it(".getPublicPEM() from public key should return public PEM string", function(){ - assert.equal(publicNodeRSA.getPublicPEM(), publicKeyPEM); + it(".exportPublic() from public key should return public PEM string", function(){ + assert.equal(publicNodeRSA.exportPublic(), publicKeyPEM); }); - it(".getPublicPEM() from private key should return public PEM string", function(){ - assert.equal(privateNodeRSA.getPublicPEM(), publicKeyPEM); + it(".exportPublic() from private key should return public PEM string", function(){ + assert.equal(privateNodeRSA.exportPublic(), publicKeyPEM); }); it("should create key from buffer/fs.readFileSync output", function(){ var key = new NodeRSA(fs.readFileSync(fileKey)); - assert.equal(key.getPrivatePEM(), fileKeyPEM); + assert.equal(key.exportPrivate(), fileKeyPEM); + key = new NodeRSA(); + key.importKey(fs.readFileSync(fileKey)); + assert.equal(key.exportPrivate(), fileKeyPEM); }); it("should load PEM from buffer/fs.readFileSync output", function(){ var key = new NodeRSA(); assert.equal(key.isEmpty(), true); - key.loadFromPEM(fs.readFileSync(fileKey)); + key.importKey(fs.readFileSync(fileKey)); assert.equal(key.isEmpty(), false); - assert.equal(key.getPrivatePEM(), fileKeyPEM); + assert.equal(key.exportPrivate(), fileKeyPEM); }); }); describe("Bad cases", function () { it("not public key", function(){ var key = new NodeRSA(); - assert.throw(function(){ key.getPrivatePEM(); }, Error, "It is not private key"); - assert.throw(function(){ key.getPublicPEM(); }, Error, "It is not public key"); + assert.throw(function(){ key.exportPrivate(); }, Error, "It is not private key"); + assert.throw(function(){ key.exportPublic(); }, Error, "It is not public key"); }); it("not private key", function(){ var key = new NodeRSA(publicKeyPEM); - assert.throw(function(){ key.getPrivatePEM(); }, Error, "It is not private key"); - assert.doesNotThrow(function(){ key.getPublicPEM(); }, Error, "It is not public key"); + assert.throw(function(){ key.exportPrivate(); }, Error, "It is not private key"); + assert.doesNotThrow(function(){ key.exportPublic(); }, Error, "It is not public key"); }); }); }); @@ -352,7 +355,7 @@ describe("NodeRSA", function(){ (function (i) { var suit = dataBundle[i]; it("should sign " + i, function () { - key = new NodeRSA(generatedKeys[generatedKeys.length - 1].getPrivatePEM(), { + key = new NodeRSA(generatedKeys[generatedKeys.length - 1].exportPrivate(), { signingScheme: scheme + '-sha256', environment: env }); @@ -373,7 +376,7 @@ describe("NodeRSA", function(){ for (var alg in signHashAlgorithms[env]) { (function (alg) { it("signing with custom algorithm (" + alg + ")", function () { - var key = new NodeRSA(generatedKeys[generatedKeys.length - 1].getPrivatePEM(), { + var key = new NodeRSA(generatedKeys[generatedKeys.length - 1].exportPrivate(), { signingScheme: scheme + '-' + alg, environment: env }); @@ -389,7 +392,7 @@ describe("NodeRSA", function(){ describe("Bad cases" + (envs.length > 1 ? " in " + env + " environment" : ""), function () { it("incorrect data for verifying", function () { - var key = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + var key = new NodeRSA(generatedKeys[0].exportPrivate(), { signingScheme: scheme + '-sha256', environment: env }); @@ -398,7 +401,7 @@ describe("NodeRSA", function(){ }); it("incorrect key for signing", function () { - var key = new NodeRSA(generatedKeys[0].getPublicPEM(), { + var key = new NodeRSA(generatedKeys[0].exportPublic(), { signingScheme: scheme + '-sha256', environment: env }); @@ -408,11 +411,11 @@ describe("NodeRSA", function(){ }); it("incorrect key for verifying", function () { - var key1 = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + var key1 = new NodeRSA(generatedKeys[0].exportPrivate(), { signingScheme: scheme + '-sha256', environment: env }); - var key2 = new NodeRSA(generatedKeys[1].getPublicPEM(), { + var key2 = new NodeRSA(generatedKeys[1].exportPublic(), { signingScheme: scheme + '-sha256', environment: env }); @@ -429,11 +432,11 @@ describe("NodeRSA", function(){ }); it("different algorithms", function () { - var singKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + var singKey = new NodeRSA(generatedKeys[0].exportPrivate(), { signingScheme: scheme + '-md5', environment: env }); - var verifyKey = new NodeRSA(generatedKeys[0].getPrivatePEM(), { + var verifyKey = new NodeRSA(generatedKeys[0].exportPrivate(), { signingScheme: scheme + '-sha1', environment: env }); @@ -452,11 +455,11 @@ describe("NodeRSA", 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(), { + var nodeKey = new NodeRSA(generatedKeys[5].exportPrivate(), { signingScheme: scheme + '-' + alg, environment: 'node' }); - var browserKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + var browserKey = new NodeRSA(generatedKeys[5].exportPrivate(), { signingScheme: scheme + '-' + alg, environment: 'browser' }); @@ -465,11 +468,11 @@ describe("NodeRSA", function(){ }); it("sign in node & verify in browser (" + alg + ")", function () { - var nodeKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + var nodeKey = new NodeRSA(generatedKeys[5].exportPrivate(), { signingScheme: scheme + '-' + alg, environment: 'node' }); - var browserKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + var browserKey = new NodeRSA(generatedKeys[5].exportPrivate(), { signingScheme: scheme + '-' + alg, environment: 'browser' }); @@ -478,11 +481,11 @@ describe("NodeRSA", function(){ }); it("sign in browser & verify in node (" + alg + ")", function () { - var nodeKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + var nodeKey = new NodeRSA(generatedKeys[5].exportPrivate(), { signingScheme: scheme + '-' + alg, environment: 'node' }); - var browserKey = new NodeRSA(generatedKeys[5].getPrivatePEM(), { + var browserKey = new NodeRSA(generatedKeys[5].exportPrivate(), { signingScheme: scheme + '-' + alg, environment: 'browser' });