-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2020 from cmgustavo/feat/bitcore-lib-message-01
Feat(bitcore-lib): adds support for verifying and signing bitcoin messages
- Loading branch information
Showing
3 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
'use strict'; | ||
|
||
var _ = require('lodash'); | ||
var PrivateKey = require('./privatekey'); | ||
var PublicKey = require('./publickey'); | ||
var Address = require('./address'); | ||
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'); | ||
var $ = require('./util/preconditions'); | ||
|
||
|
||
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; | ||
|
||
var Script = require('./script'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
'use strict'; | ||
|
||
/* jshint maxstatements: 30 */ | ||
|
||
var chai = require('chai'); | ||
var should = chai.should(); | ||
var expect = chai.expect; | ||
|
||
var bitcore = require('..'); | ||
var Address = bitcore.Address; | ||
var Signature = bitcore.crypto.Signature; | ||
var Message = require('../lib/message'); | ||
|
||
describe('Message', function() { | ||
|
||
var address = 'n1ZCYg9YXtB5XCZazLxSmPDa8iwJRZHhGx'; | ||
var badAddress = 'mmRcrB5fTwgxaFJmVLNtaG8SV454y1E3kC'; | ||
var privateKey = bitcore.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 bitcore.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); | ||
}); | ||
|
||
}); |