239 lines
8.4 KiB
JavaScript
239 lines
8.4 KiB
JavaScript
/**
|
|
* PKCS1 padding and signature scheme
|
|
*/
|
|
|
|
var BigInteger = require('../libs/jsbn');
|
|
var crypt = require('crypto');
|
|
var constants = require('constants');
|
|
var SIGN_INFO_HEAD = {
|
|
md2: Buffer.from('3020300c06082a864886f70d020205000410', 'hex'),
|
|
md5: Buffer.from('3020300c06082a864886f70d020505000410', 'hex'),
|
|
sha1: Buffer.from('3021300906052b0e03021a05000414', 'hex'),
|
|
sha224: Buffer.from('302d300d06096086480165030402040500041c', 'hex'),
|
|
sha256: Buffer.from('3031300d060960864801650304020105000420', 'hex'),
|
|
sha384: Buffer.from('3041300d060960864801650304020205000430', 'hex'),
|
|
sha512: Buffer.from('3051300d060960864801650304020305000440', 'hex'),
|
|
ripemd160: Buffer.from('3021300906052b2403020105000414', 'hex'),
|
|
rmd160: Buffer.from('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 () {
|
|
if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) {
|
|
return this.key.encryptedDataLength;
|
|
}
|
|
return this.key.encryptedDataLength - 11;
|
|
};
|
|
|
|
/**
|
|
* Pad input Buffer to encryptedDataLength bytes, and return Buffer.from
|
|
* alg: PKCS#1
|
|
* @param buffer
|
|
* @returns {Buffer}
|
|
*/
|
|
Scheme.prototype.encPad = function (buffer, options) {
|
|
options = options || {};
|
|
var filled;
|
|
if (buffer.length > this.key.maxMessageLength) {
|
|
throw new Error("Message too long for RSA (n=" + this.key.encryptedDataLength + ", l=" + buffer.length + ")");
|
|
}
|
|
if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) {
|
|
//RSA_NO_PADDING treated like JAVA left pad with zero character
|
|
filled = Buffer.alloc(this.key.maxMessageLength - buffer.length);
|
|
filled.fill(0);
|
|
return Buffer.concat([filled, buffer]);
|
|
}
|
|
|
|
/* Type 1: zeros padding for private key encrypt */
|
|
if (options.type === 1) {
|
|
filled = Buffer.alloc(this.key.encryptedDataLength - buffer.length - 1);
|
|
filled.fill(0xff, 0, filled.length - 1);
|
|
filled[0] = 1;
|
|
filled[filled.length - 1] = 0;
|
|
|
|
return Buffer.concat([filled, buffer]);
|
|
} else {
|
|
/* random padding for public key encrypt */
|
|
filled = Buffer.alloc(this.key.encryptedDataLength - buffer.length);
|
|
filled[0] = 0;
|
|
filled[1] = 2;
|
|
var rand = crypt.randomBytes(filled.length - 3);
|
|
for (var i = 0; i < rand.length; i++) {
|
|
var r = rand[i];
|
|
while (r === 0) { // non-zero only
|
|
r = crypt.randomBytes(1)[0];
|
|
}
|
|
filled[i + 2] = r;
|
|
}
|
|
filled[filled.length - 1] = 0;
|
|
return Buffer.concat([filled, buffer]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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, options) {
|
|
options = options || {};
|
|
var i = 0;
|
|
|
|
if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) {
|
|
//RSA_NO_PADDING treated like JAVA left pad with zero character
|
|
var unPad;
|
|
if (typeof buffer.lastIndexOf == "function") { //patch for old node version
|
|
unPad = buffer.slice(buffer.lastIndexOf('\0') + 1, buffer.length);
|
|
} else {
|
|
unPad = buffer.slice(String.prototype.lastIndexOf.call(buffer, '\0') + 1, buffer.length);
|
|
}
|
|
return unPad;
|
|
}
|
|
|
|
if (buffer.length < 4) {
|
|
return null;
|
|
}
|
|
|
|
/* Type 1: zeros padding for private key decrypt */
|
|
if (options.type === 1) {
|
|
if (buffer[0] !== 0 && buffer[1] !== 1) {
|
|
return null;
|
|
}
|
|
i = 3;
|
|
while (buffer[i] !== 0) {
|
|
if (buffer[i] != 0xFF || ++i >= buffer.length) {
|
|
return null;
|
|
}
|
|
}
|
|
} else {
|
|
/* random padding for public key decrypt */
|
|
if (buffer[0] !== 0 && buffer[1] !== 2) {
|
|
return null;
|
|
}
|
|
i = 3;
|
|
while (buffer[i] !== 0) {
|
|
if (++i >= buffer.length) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
return buffer.slice(i + 1, buffer.length);
|
|
};
|
|
|
|
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.exportKey('private'));
|
|
}
|
|
};
|
|
|
|
Scheme.prototype.verify = function (buffer, signature, signature_encoding) {
|
|
if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) {
|
|
//RSA_NO_PADDING has no verify data
|
|
return false;
|
|
}
|
|
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 = Buffer.from(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.exportKey('public'), signature, signature_encoding);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* PKCS#1 zero pad input buffer to max data length
|
|
* @param hashBuf
|
|
* @param hashAlgorithm
|
|
* @returns {*}
|
|
*/
|
|
Scheme.prototype.pkcs0pad = function (buffer) {
|
|
var filled = Buffer.alloc(this.key.maxMessageLength - buffer.length);
|
|
filled.fill(0);
|
|
return Buffer.concat([filled, buffer]);
|
|
};
|
|
|
|
Scheme.prototype.pkcs0unpad = function (buffer) {
|
|
var unPad;
|
|
if (typeof buffer.lastIndexOf == "function") { //patch for old node version
|
|
unPad = buffer.slice(buffer.lastIndexOf('\0') + 1, buffer.length);
|
|
} else {
|
|
unPad = buffer.slice(String.prototype.lastIndexOf.call(buffer, '\0') + 1, buffer.length);
|
|
}
|
|
|
|
return unPad;
|
|
};
|
|
|
|
/**
|
|
* 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 = Buffer.alloc(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);
|
|
};
|
|
|
|
|