Emilio Almansi
7 years ago
9 changed files with 528 additions and 80 deletions
@ -0,0 +1,166 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var $ = require('./util/preconditions'); |
|||
var Address = require('./address'); |
|||
var PublicKey = require('./publickey'); |
|||
var PrivateKey = require('./privatekey'); |
|||
var BufferWriter = require('./encoding/bufferwriter'); |
|||
var ECDSA = require('./crypto/ecdsa'); |
|||
var Signature = require('./crypto/signature'); |
|||
var sha256sha256 = require('./crypto/hash').sha256sha256; |
|||
var JSUtil = require('./util/js'); |
|||
|
|||
/** |
|||
* constructs a new message to sign and verify. |
|||
* |
|||
* @param {String} message |
|||
* @returns {Message} |
|||
*/ |
|||
var Message = function Message(message) { |
|||
if (!(this instanceof Message)) { |
|||
return new Message(message); |
|||
} |
|||
$.checkArgument(_.isString(message), 'First argument should be a string'); |
|||
this.message = message; |
|||
|
|||
return this; |
|||
}; |
|||
|
|||
Message.MAGIC_BYTES = new Buffer('Bitcoin Signed Message:\n'); |
|||
|
|||
Message.prototype.magicHash = function magicHash() { |
|||
var prefix1 = BufferWriter.varintBufNum(Message.MAGIC_BYTES.length); |
|||
var messageBuffer = new Buffer(this.message); |
|||
var prefix2 = BufferWriter.varintBufNum(messageBuffer.length); |
|||
var buf = Buffer.concat([prefix1, Message.MAGIC_BYTES, prefix2, messageBuffer]); |
|||
var hash = sha256sha256(buf); |
|||
return hash; |
|||
}; |
|||
|
|||
Message.prototype._sign = function _sign(privateKey) { |
|||
$.checkArgument(privateKey instanceof PrivateKey, 'First argument should be an instance of PrivateKey'); |
|||
var hash = this.magicHash(); |
|||
var ecdsa = new ECDSA(); |
|||
ecdsa.hashbuf = hash; |
|||
ecdsa.privkey = privateKey; |
|||
ecdsa.pubkey = privateKey.toPublicKey(); |
|||
ecdsa.signRandomK(); |
|||
ecdsa.calci(); |
|||
return ecdsa.sig; |
|||
}; |
|||
|
|||
/** |
|||
* Will sign a message with a given bitcoin private key. |
|||
* |
|||
* @param {PrivateKey} privateKey - An instance of PrivateKey |
|||
* @returns {String} A base64 encoded compact signature |
|||
*/ |
|||
Message.prototype.sign = function sign(privateKey) { |
|||
var signature = this._sign(privateKey); |
|||
return signature.toCompact().toString('base64'); |
|||
}; |
|||
|
|||
Message.prototype._verify = function _verify(publicKey, signature) { |
|||
$.checkArgument(publicKey instanceof PublicKey, 'First argument should be an instance of PublicKey'); |
|||
$.checkArgument(signature instanceof Signature, 'Second argument should be an instance of Signature'); |
|||
var hash = this.magicHash(); |
|||
var verified = ECDSA.verify(hash, signature, publicKey); |
|||
if (!verified) { |
|||
this.error = 'The signature was invalid'; |
|||
} |
|||
return verified; |
|||
}; |
|||
|
|||
/** |
|||
* Will return a boolean of the signature is valid for a given bitcoin address. |
|||
* If it isn't the specific reason is accessible via the "error" member. |
|||
* |
|||
* @param {Address|String} bitcoinAddress - A bitcoin address |
|||
* @param {String} signatureString - A base64 encoded compact signature |
|||
* @returns {Boolean} |
|||
*/ |
|||
Message.prototype.verify = function verify(bitcoinAddress, signatureString) { |
|||
$.checkArgument(bitcoinAddress); |
|||
$.checkArgument(signatureString && _.isString(signatureString)); |
|||
|
|||
if (_.isString(bitcoinAddress)) { |
|||
bitcoinAddress = Address.fromString(bitcoinAddress); |
|||
} |
|||
var signature = Signature.fromCompact(new Buffer(signatureString, 'base64')); |
|||
|
|||
// recover the public key
|
|||
var ecdsa = new ECDSA(); |
|||
ecdsa.hashbuf = this.magicHash(); |
|||
ecdsa.sig = signature; |
|||
var publicKey = ecdsa.toPublicKey(); |
|||
|
|||
var signatureAddress = Address.fromPublicKey(publicKey, bitcoinAddress.network); |
|||
|
|||
// check that the recovered address and specified address match
|
|||
if (bitcoinAddress.toString() !== signatureAddress.toString()) { |
|||
this.error = 'The signature did not match the message digest'; |
|||
return false; |
|||
} |
|||
|
|||
return this._verify(publicKey, signature); |
|||
}; |
|||
|
|||
/** |
|||
* Instantiate a message from a message string |
|||
* |
|||
* @param {String} str - A string of the message |
|||
* @returns {Message} A new instance of a Message |
|||
*/ |
|||
Message.fromString = function (str) { |
|||
return new Message(str); |
|||
}; |
|||
|
|||
/** |
|||
* Instantiate a message from JSON |
|||
* |
|||
* @param {String} json - An JSON string or Object with keys: message |
|||
* @returns {Message} A new instance of a Message |
|||
*/ |
|||
Message.fromJSON = function fromJSON(json) { |
|||
if (JSUtil.isValidJSON(json)) { |
|||
json = JSON.parse(json); |
|||
} |
|||
return new Message(json.message); |
|||
}; |
|||
|
|||
/** |
|||
* @returns {Object} A plain object with the message information |
|||
*/ |
|||
Message.prototype.toObject = function toObject() { |
|||
return { |
|||
message: this.message |
|||
}; |
|||
}; |
|||
|
|||
/** |
|||
* @returns {String} A JSON representation of the message information |
|||
*/ |
|||
Message.prototype.toJSON = function toJSON() { |
|||
return JSON.stringify(this.toObject()); |
|||
}; |
|||
|
|||
/** |
|||
* Will return a the string representation of the message |
|||
* |
|||
* @returns {String} Message |
|||
*/ |
|||
Message.prototype.toString = function () { |
|||
return this.message; |
|||
}; |
|||
|
|||
/** |
|||
* Will return a string formatted for the console |
|||
* |
|||
* @returns {String} Message |
|||
*/ |
|||
Message.prototype.inspect = function () { |
|||
return '<Message: ' + this.toString() + '>'; |
|||
}; |
|||
|
|||
module.exports = Message; |
@ -0,0 +1,168 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var $ = require('./util/preconditions'); |
|||
var Address = require('./address'); |
|||
var PublicKey = require('./publickey'); |
|||
var PrivateKey = require('./privatekey'); |
|||
var BufferWriter = require('./encoding/bufferwriter'); |
|||
var ECDSA = require('./crypto/ecdsa'); |
|||
var Signature = require('./crypto/signature'); |
|||
var sha256sha256 = require('./crypto/hash').sha256sha256; |
|||
var JSUtil = require('./util/js'); |
|||
|
|||
/** |
|||
* constructs a new message to sign and verify. |
|||
* |
|||
* @param {String} message |
|||
* @returns {Message} |
|||
*/ |
|||
var Message = function Message(message) { |
|||
if (!(this instanceof Message)) { |
|||
return new Message(message); |
|||
} |
|||
$.checkArgument(_.isString(message), 'First argument should be a string'); |
|||
this.message = message; |
|||
|
|||
return this; |
|||
}; |
|||
|
|||
Message.MAGIC_BYTES = new Buffer('Bitcoin Signed Message:\n'); |
|||
|
|||
Message.prototype.magicHash = function magicHash() { |
|||
var prefix1 = BufferWriter.varintBufNum(Message.MAGIC_BYTES.length); |
|||
var messageBuffer = new Buffer(this.message); |
|||
var prefix2 = BufferWriter.varintBufNum(messageBuffer.length); |
|||
var buf = Buffer.concat([prefix1, Message.MAGIC_BYTES, prefix2, messageBuffer]); |
|||
var hash = sha256sha256(buf); |
|||
return hash; |
|||
}; |
|||
|
|||
Message.prototype._sign = function _sign(privateKey) { |
|||
console.log(privateKey); |
|||
$.checkArgument(privateKey instanceof PrivateKey, |
|||
'First argument should be an instance of PrivateKey'); |
|||
var hash = this.magicHash(); |
|||
var ecdsa = new ECDSA(); |
|||
ecdsa.hashbuf = hash; |
|||
ecdsa.privkey = privateKey; |
|||
ecdsa.pubkey = privateKey.toPublicKey(); |
|||
ecdsa.signRandomK(); |
|||
ecdsa.calci(); |
|||
return ecdsa.sig; |
|||
}; |
|||
|
|||
/** |
|||
* Will sign a message with a given bitcoin private key. |
|||
* |
|||
* @param {PrivateKey} privateKey - An instance of PrivateKey |
|||
* @returns {String} A base64 encoded compact signature |
|||
*/ |
|||
Message.prototype.sign = function sign(privateKey) { |
|||
var signature = this._sign(privateKey); |
|||
return signature.toCompact().toString('base64'); |
|||
}; |
|||
|
|||
Message.prototype._verify = function _verify(publicKey, signature) { |
|||
$.checkArgument(publicKey instanceof PublicKey, 'First argument should be an instance of PublicKey'); |
|||
$.checkArgument(signature instanceof Signature, 'Second argument should be an instance of Signature'); |
|||
var hash = this.magicHash(); |
|||
var verified = ECDSA.verify(hash, signature, publicKey); |
|||
if (!verified) { |
|||
this.error = 'The signature was invalid'; |
|||
} |
|||
return verified; |
|||
}; |
|||
|
|||
/** |
|||
* Will return a boolean of the signature is valid for a given bitcoin address. |
|||
* If it isn't the specific reason is accessible via the "error" member. |
|||
* |
|||
* @param {Address|String} bitcoinAddress - A bitcoin address |
|||
* @param {String} signatureString - A base64 encoded compact signature |
|||
* @returns {Boolean} |
|||
*/ |
|||
Message.prototype.verify = function verify(bitcoinAddress, signatureString) { |
|||
$.checkArgument(bitcoinAddress); |
|||
$.checkArgument(signatureString && _.isString(signatureString)); |
|||
|
|||
if (_.isString(bitcoinAddress)) { |
|||
bitcoinAddress = Address.fromString(bitcoinAddress); |
|||
} |
|||
var signature = Signature.fromCompact(new Buffer(signatureString, 'base64')); |
|||
|
|||
// recover the public key
|
|||
var ecdsa = new ECDSA(); |
|||
ecdsa.hashbuf = this.magicHash(); |
|||
ecdsa.sig = signature; |
|||
var publicKey = ecdsa.toPublicKey(); |
|||
|
|||
var signatureAddress = Address.fromPublicKey(publicKey, bitcoinAddress.network); |
|||
|
|||
// check that the recovered address and specified address match
|
|||
if (bitcoinAddress.toString() !== signatureAddress.toString()) { |
|||
this.error = 'The signature did not match the message digest'; |
|||
return false; |
|||
} |
|||
|
|||
return this._verify(publicKey, signature); |
|||
}; |
|||
|
|||
/** |
|||
* Instantiate a message from a message string |
|||
* |
|||
* @param {String} str - A string of the message |
|||
* @returns {Message} A new instance of a Message |
|||
*/ |
|||
Message.fromString = function(str) { |
|||
return new Message(str); |
|||
}; |
|||
|
|||
/** |
|||
* Instantiate a message from JSON |
|||
* |
|||
* @param {String} json - An JSON string or Object with keys: message |
|||
* @returns {Message} A new instance of a Message |
|||
*/ |
|||
Message.fromJSON = function fromJSON(json) { |
|||
if (JSUtil.isValidJSON(json)) { |
|||
json = JSON.parse(json); |
|||
} |
|||
return new Message(json.message); |
|||
}; |
|||
|
|||
/** |
|||
* @returns {Object} A plain object with the message information |
|||
*/ |
|||
Message.prototype.toObject = function toObject() { |
|||
return { |
|||
message: this.message |
|||
}; |
|||
}; |
|||
|
|||
/** |
|||
* @returns {String} A JSON representation of the message information |
|||
*/ |
|||
Message.prototype.toJSON = function toJSON() { |
|||
return JSON.stringify(this.toObject()); |
|||
}; |
|||
|
|||
/** |
|||
* Will return a the string representation of the message |
|||
* |
|||
* @returns {String} Message |
|||
*/ |
|||
Message.prototype.toString = function() { |
|||
return this.message; |
|||
}; |
|||
|
|||
/** |
|||
* Will return a string formatted for the console |
|||
* |
|||
* @returns {String} Message |
|||
*/ |
|||
Message.prototype.inspect = function() { |
|||
return '<Message: ' + this.toString() + '>'; |
|||
}; |
|||
|
|||
module.exports = Message; |
@ -0,0 +1,164 @@ |
|||
'use strict'; |
|||
|
|||
var chai = require('chai'); |
|||
var expect = chai.expect; |
|||
var should = chai.should(); |
|||
|
|||
var bch = require('..'); |
|||
var Address = bch.Address; |
|||
var Signature = bch.crypto.Signature; |
|||
var Message = bch.Message; |
|||
|
|||
describe('Message', function() { |
|||
|
|||
var address = 'n1ZCYg9YXtB5XCZazLxSmPDa8iwJRZHhGx'; |
|||
var badAddress = 'mmRcrB5fTwgxaFJmVLNtaG8SV454y1E3kC'; |
|||
var privateKey = bch.PrivateKey.fromWIF('cPBn5A4ikZvBTQ8D7NnvHZYCAxzDZ5Z2TSGW2LkyPiLxqYaJPBW4'); |
|||
var text = 'hello, world'; |
|||
var signatureString = 'H/DIn8uA1scAuKLlCx+/9LnAcJtwQQ0PmcPrJUq90aboLv3fH5fFvY+vmbfOSFEtGarznYli6ShPr9RXwY9UrIY='; |
|||
|
|||
var badSignatureString = 'H69qZ4mbZCcvXk7CWjptD5ypnYVLvQ3eMXLM8+1gX21SLH/GaFnAjQrDn37+TDw79i9zHhbiMMwhtvTwnPigZ6k='; |
|||
|
|||
var signature = Signature.fromCompact(new Buffer(signatureString, 'base64')); |
|||
var badSignature = Signature.fromCompact(new Buffer(badSignatureString, 'base64')); |
|||
|
|||
var publicKey = privateKey.toPublicKey(); |
|||
|
|||
it('will error with incorrect message type', function() { |
|||
expect(function() { |
|||
return new Message(new Date()); |
|||
}).to.throw('First argument should be a string'); |
|||
}); |
|||
|
|||
it('will instantiate without "new"', function() { |
|||
var message = Message(text); |
|||
should.exist(message); |
|||
}); |
|||
|
|||
var signature2; |
|||
var signature3; |
|||
|
|||
it('can sign a message', function() { |
|||
var message2 = new Message(text); |
|||
signature2 = message2._sign(privateKey); |
|||
signature3 = Message(text).sign(privateKey); |
|||
should.exist(signature2); |
|||
should.exist(signature3); |
|||
}); |
|||
|
|||
it('sign will error with incorrect private key argument', function() { |
|||
expect(function() { |
|||
var message3 = new Message(text); |
|||
return message3.sign('not a private key'); |
|||
}).to.throw('First argument should be an instance of PrivateKey'); |
|||
}); |
|||
|
|||
it('can verify a message with signature', function() { |
|||
var message4 = new Message(text); |
|||
var verified = message4._verify(publicKey, signature2); |
|||
verified.should.equal(true); |
|||
}); |
|||
|
|||
it('can verify a message with existing signature', function() { |
|||
var message5 = new Message(text); |
|||
var verified = message5._verify(publicKey, signature); |
|||
verified.should.equal(true); |
|||
}); |
|||
|
|||
it('verify will error with incorrect public key argument', function() { |
|||
expect(function() { |
|||
var message6 = new Message(text); |
|||
return message6._verify('not a public key', signature); |
|||
}).to.throw('First argument should be an instance of PublicKey'); |
|||
}); |
|||
|
|||
it('verify will error with incorrect signature argument', function() { |
|||
expect(function() { |
|||
var message7 = new Message(text); |
|||
return message7._verify(publicKey, 'not a signature'); |
|||
}).to.throw('Second argument should be an instance of Signature'); |
|||
}); |
|||
|
|||
it('verify will correctly identify a bad signature', function() { |
|||
var message8 = new Message(text); |
|||
var verified = message8._verify(publicKey, badSignature); |
|||
should.exist(message8.error); |
|||
verified.should.equal(false); |
|||
}); |
|||
|
|||
it('can verify a message with address and generated signature string', function() { |
|||
var message9 = new Message(text); |
|||
var verified = message9.verify(address, signature3); |
|||
should.not.exist(message9.error); |
|||
verified.should.equal(true); |
|||
}); |
|||
|
|||
it('will not verify with address mismatch', function() { |
|||
var message10 = new Message(text); |
|||
var verified = message10.verify(badAddress, signatureString); |
|||
should.exist(message10.error); |
|||
verified.should.equal(false); |
|||
}); |
|||
|
|||
it('will verify with an uncompressed pubkey', function() { |
|||
var privateKey = new bch.PrivateKey('5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss'); |
|||
var message = new Message('This is an example of a signed message.'); |
|||
var signature = message.sign(privateKey); |
|||
var verified = message.verify(privateKey.toAddress(), signature); |
|||
verified.should.equal(true); |
|||
}); |
|||
|
|||
it('can chain methods', function() { |
|||
var verified = Message(text).verify(address, signatureString); |
|||
verified.should.equal(true); |
|||
}); |
|||
|
|||
describe('#json', function() { |
|||
|
|||
it('roundtrip to-from-to', function() { |
|||
var json = new Message(text).toJSON(); |
|||
var message = Message.fromJSON(json); |
|||
message.toString().should.equal(text); |
|||
}); |
|||
|
|||
it('checks that the string parameter is valid JSON', function() { |
|||
expect(function() { |
|||
return Message.fromJSON('¹'); |
|||
}).to.throw(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
describe('#toString', function() { |
|||
|
|||
it('message string', function() { |
|||
var message = new Message(text); |
|||
message.toString().should.equal(text); |
|||
}); |
|||
|
|||
it('roundtrip to-from-to', function() { |
|||
var str = new Message(text).toString(); |
|||
var message = Message.fromString(str); |
|||
message.toString().should.equal(text); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
describe('#inspect', function() { |
|||
|
|||
it('should output formatted output correctly', function() { |
|||
var message = new Message(text); |
|||
var output = '<Message: '+text+'>'; |
|||
message.inspect().should.equal(output); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
|
|||
it('accepts Address for verification', function() { |
|||
var verified = Message(text) |
|||
.verify(new Address(address), signatureString); |
|||
verified.should.equal(true); |
|||
}); |
|||
|
|||
}); |
Loading…
Reference in new issue