diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..e28b9ec --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: OMJRNZCl018Yjy44nlG1hF6maKEyXcwPx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2c2572 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.sw[a-z] +coverage +node_modules + +npm-debug.log +bitcore-message.js +bitcore-message.min.js +tests.js + +report +.DS_Store diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..82597a3 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,44 @@ +{ + "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). + "browser": true, // Standard browser globals e.g. `window`, `document`. + "camelcase": false, // Permit only camelcase for `var` and `object indexes`. + "curly": true, // Require {} for every new block or scope. + "devel": false, // Allow development statements e.g. `console.log();`. + "eqeqeq": true, // Require triple equals i.e. `===`. + "esnext": true, // Allow ES.next specific features such as `const` and `let`. + "freeze": true, // Forbid overwriting prototypes of native objects such as Array, Date and so on. + "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` + "indent": 2, // Specify indentation spacing + "latedef": true, // Prohibit variable use before definition. + "newcap": false, // Require capitalization of all constructor functions e.g. `new F()`. + "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. + "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. + "noempty": true, // Prohibit use of empty blocks. + "nonew": true, // Prohibits the use of constructor functions for side-effects + "quotmark": "single", // Define quotes to string values. + "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. + "smarttabs": false, // Supress warnings about mixed tabs and spaces + "strict": true, // Require `use strict` pragma in every file. + "trailing": true, // Prohibit trailing whitespaces. + "undef": true, // Require all non-global variables be declared before they are used. + "unused": true, // Warn unused variables. + + "maxparams": 4, // Maximum number of parameters for a function + "maxstatements": 15, // Maximum number of statements in a function + "maxcomplexity": 6, // Cyclomatic complexity (http://en.wikipedia.org/wiki/Cyclomatic_complexity) + "maxdepth": 4, // Maximum depth of nested control structures + "maxlen": 120, // Maximum number of cols in a line + + "predef": [ // Extra globals. + "after", + "afterEach", + "before", + "beforeEach", + "define", + "describe", + "exports", + "it", + "module", + "require" + ] +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fe29353 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: +- '0.10' +before_install: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +install: + - npm install +after_script: + - gulp coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e5fcc1d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +Please see [CONTRIBUTING.md](https://github.com/bitpay/bitcore/blob/master/CONTRIBUTING.md) on the main bitcore repo. diff --git a/README.md b/README.md index 8b13789..0b06bba 100644 --- a/README.md +++ b/README.md @@ -1 +1,46 @@ +======= +Bitcoin Message Verification and Signing for Bitcore +======= + +[![NPM Package](https://img.shields.io/npm/v/bitcore-message.svg?style=flat-square)](https://www.npmjs.org/package/bitcore-message) +[![Build Status](https://img.shields.io/travis/bitpay/bitcore-message.svg?branch=master&style=flat-square)](https://travis-ci.org/bitpay/bitcore-message) +[![Coverage Status](https://img.shields.io/coveralls/bitpay/bitcore-message.svg?style=flat-square)](https://coveralls.io/r/bitpay/bitcore-message?branch=master) + +bitcore-message adds support for verifying and signing bitcoin messages in [Node.js](http://nodejs.org/) and web browsers. + +See [the main bitcore repo](https://github.com/bitpay/bitcore) for more information. + +## Getting Started + +```sh +npm install bitcore-message +``` + +```sh +bower install bitcore-message +``` + +To sign a message: + +```javascript +var privateKey = PrivateKey.fromWIF('cPBn5A4ikZvBTQ8D7NnvHZYCAxzDZ5Z2TSGW2LkyPiLxqYaJPBW4'); +var signature = Message('hello, world').sign(privateKey); +``` + +To verify a message: + +```javascript +var address = 'n1ZCYg9YXtB5XCZazLxSmPDa8iwJRZHhGx'; +var verified = Message('hello, world').verify(address, signature); +``` + +## Contributing + +See [CONTRIBUTING.md](https://github.com/bitpay/bitcore) on the main bitcore repo for information about how to contribute. + +## License + +Code released under [the MIT license](https://github.com/bitpay/bitcore/blob/master/LICENSE). + +Copyright 2013-2015 BitPay, Inc. Bitcore is a trademark maintained by BitPay, Inc. diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..9abecf7 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,8 @@ +'use strict'; + +var gulp = require('gulp'); +var bitcoreTasks = require('bitcore-build'); + +bitcoreTasks('message'); + +gulp.task('default', ['lint', 'coverage']); diff --git a/index.js b/index.js new file mode 100644 index 0000000..fa9d743 --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +var bitcore = require('bitcore'); +bitcore.Message = require('./lib/message'); + +module.exports = bitcore.Message; \ No newline at end of file diff --git a/lib/message.js b/lib/message.js new file mode 100644 index 0000000..b12afb5 --- /dev/null +++ b/lib/message.js @@ -0,0 +1,111 @@ +'use strict'; + +var bitcore = require('bitcore'); +var PrivateKey = bitcore.PrivateKey; +var PublicKey = bitcore.PublicKey; +var Address = bitcore.Address; +var BufferWriter = bitcore.encoding.BufferWriter; +var ECDSA = bitcore.crypto.ECDSA; +var Signature = bitcore.crypto.Signature; +var sha256sha256 = bitcore.crypto.Hash.sha256sha256; + +/** + * Will construct 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); + } + if (typeof message !== 'string') { + throw new TypeError('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) { + if (!(privateKey instanceof PrivateKey)) { + throw new TypeError('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) { + if (!(publicKey instanceof PublicKey)) { + throw new TypeError('First argument should be an instance of PublicKey'); + } + if (!(signature instanceof Signature)) { + throw new TypeError('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 {String} bitcoinAddress - A bitcoin address + * @param {String} signatureString - A base64 encoded compact signature + * @returns {Boolean} + */ +Message.prototype.verify = function verify(bitcoinAddress, signatureString) { + 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 expectedAddress = Address.fromString(bitcoinAddress); + var signatureAddress = Address.fromPublicKey(publicKey, expectedAddress.network); + + // check that the recovered address and specified address match + if (expectedAddress.toString() !== signatureAddress.toString()) { + this.error = 'The signature did not match the message digest'; + return false; + } + + return this._verify(publicKey, signature); +}; + +module.exports = Message; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa5e9ab --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "bitcore-message", + "version": "0.9.0", + "description": "Bitcoin Messages for Bitcore", + "author": "BitPay ", + "main": "index.js", + "scripts": { + "lint": "gulp lint", + "test": "gulp test:node", + "coverage": "gulp coverage", + "build": "gulp" + }, + "keywords": [ + "bitcoin", + "bitcore" + ], + "repository": { + "type": "git", + "url": "https://github.com/bitpay/bitcore-message.git" + }, + "dependencies": { + "bitcore": "^0.9.4" + }, + "devDependencies": { + "bitcore-build": "bitpay/bitcore-build", + "brfs": "^1.3.0", + "chai": "~1.10.0", + "gulp": "^3.8.10" + }, + "license": "MIT" +} diff --git a/test/message.js b/test/message.js new file mode 100644 index 0000000..c37e5d2 --- /dev/null +++ b/test/message.js @@ -0,0 +1,107 @@ +'use strict'; + +var chai = require('chai'); +var expect = chai.expect; +var should = chai.should(); + +var bitcore = require('bitcore'); +var Signature = bitcore.crypto.Signature; +var Message = require('../'); + +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(TypeError); + }); + + 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(TypeError); + }); + + 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(TypeError); + }); + + 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(TypeError); + }); + + 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('can chain methods', function() { + var verified = Message(text).verify(address, signatureString); + verified.should.equal(true); + }); + +});