diff --git a/lib/hdpublickey.js b/lib/hdpublickey.js index da975ab3a..e94701609 100644 --- a/lib/hdpublickey.js +++ b/lib/hdpublickey.js @@ -9,16 +9,11 @@ var HDPrivateKey = require('./hdprivatekey'); var Network = require('./networks'); var Point = require('./crypto/point'); var PublicKey = require('./publickey'); -var Random = require('./crypto/random'); var assert = require('assert'); var buffer = require('buffer'); var util = require('./util'); -var MINIMUM_ENTROPY_BITS = 128; -var BITS_TO_BYTES = 1/8; -var MAXIMUM_ENTROPY_BITS = 512; - function HDPublicKey(arg) { /* jshint maxcomplexity: 12 */ @@ -32,7 +27,9 @@ function HDPublicKey(arg) { if (arg) { if (_.isString(arg) || buffer.Buffer.isBuffer(arg)) { if (HDPublicKey.isValidSerialized(arg)) { - this._buildFromSerialized(arg); + return this._buildFromSerialized(arg); + } else if (util.isValidJson(arg)) { + return this._buildFromJson(arg); } else { var error = HDPublicKey.getSerializedError(arg); if (error === HDPublicKey.Errors.ArgumentIsPrivateExtended) { @@ -43,18 +40,16 @@ function HDPublicKey(arg) { } else { if (_.isObject(arg)) { if (arg instanceof HDPrivateKey) { - this._buildFromPrivate(arg); + return this._buildFromPrivate(arg); } else { - this._buildFromObject(arg); + return this._buildFromObject(arg); } - } else if (util.isValidJson(arg)) { - this._buildFromJson(arg); } else { throw new Error(HDPublicKey.Errors.UnrecognizedArgument); } } } else { - this._generateRandomly(); + throw new Error(HDPublicKey.Errors.MustSupplyArgument); } } @@ -137,7 +132,7 @@ HDPublicKey.isValidSerialized = function (data, network) { */ HDPublicKey.getSerializedError = function (data, network) { /* jshint maxcomplexity: 10 */ - network = Network.get(network) || Network.defaultNetwork; + /* jshint maxstatements: 20 */ if (!(_.isString(data) || buffer.Buffer.isBuffer(data))) { return HDPublicKey.Errors.InvalidArgument; } @@ -152,15 +147,16 @@ HDPublicKey.getSerializedError = function (data, network) { if (data.length !== 78) { return HDPublicKey.Errors.InvalidLength; } - if (util.integerFromBuffer(data.slice(0, 4)) === network.xprivkey) { - return HDPublicKey.Errors.ArgumentIsPrivateExtended; - } if (!_.isUndefined(network)) { var error = HDPublicKey._validateNetwork(data, network); if (error) { return error; } } + network = Network.get(network) || Network.defaultNetwork; + if (util.integerFromBuffer(data.slice(0, 4)) === network.xprivkey) { + return HDPublicKey.Errors.ArgumentIsPrivateExtended; + } return null; }; @@ -192,17 +188,17 @@ HDPublicKey.prototype._buildFromPrivate = function (arg) { }; HDPublicKey.prototype._buildFromObject = function (arg) { - /* jshint maxcomplexity: 8 */ + /* jshint maxcomplexity: 10 */ // TODO: Type validation var buffers = { - version: util.integerAsBuffer(Network.get(arg.network).xpubkey), + version: arg.network ? util.integerAsBuffer(Network.get(arg.network).xpubkey) : arg.version, depth: util.integerAsSingleByteBuffer(arg.depth), parentFingerPrint: _.isNumber(arg.parentFingerPrint) ? util.integerAsBuffer(arg.parentFingerPrint) : arg.parentFingerPrint, childIndex: util.integerAsBuffer(arg.childIndex), chainCode: _.isString(arg.chainCode) ? util.hexToBuffer(arg.chainCode) : arg.chainCode, publicKey: _.isString(arg.publicKey) ? util.hexToBuffer(arg.publicKey) : buffer.Buffer.isBuffer(arg.publicKey) ? arg.publicKey : arg.publicKey.toBuffer(), - checksum: arg.checksum && arg.checksum.length ? util.integerAsBuffer(arg.checksum) : undefined + checksum: _.isNumber(arg.checksum) ? util.integerAsBuffer(arg.checksum) : arg.checksum }; return this._buildFromBuffers(buffers); }; @@ -223,37 +219,6 @@ HDPublicKey.prototype._buildFromSerialized = function (arg) { return this._buildFromBuffers(buffers); }; -HDPublicKey.prototype._generateRandomly = function (network) { - return HDPublicKey.fromSeed(Random.getRandomBytes(64), network); -}; - -HDPublicKey.fromSeed = function (hexa, network) { - /* jshint maxcomplexity: 8 */ - - if (util.isHexaString(hexa)) { - hexa = util.hexToBuffer(hexa); - } - if (!Buffer.isBuffer(hexa)) { - throw new Error(HDPublicKey.InvalidEntropyArg); - } - if (hexa.length < MINIMUM_ENTROPY_BITS * BITS_TO_BYTES) { - throw new Error(HDPublicKey.NotEnoughEntropy); - } - if (hexa.length > MAXIMUM_ENTROPY_BITS * BITS_TO_BYTES) { - throw new Error('More than 512 bytes of entropy is nonstandard'); - } - var hash = Hash.sha512hmac(hexa, new buffer.Buffer('Bitcoin seed')); - - return new HDPublicKey({ - network: Network.get(network) || Network.livenet, - depth: 0, - parentFingerPrint: 0, - childIndex: 0, - publicKey: hash.slice(0, 32), - chainCode: hash.slice(32, 64) - }); -}; - /** * Receives a object with buffers in all the properties and populates the * internal structure @@ -272,6 +237,7 @@ HDPublicKey.fromSeed = function (hexa, network) { */ HDPublicKey.prototype._buildFromBuffers = function (arg) { /* jshint maxcomplexity: 8 */ + /* jshint maxstatements: 20 */ HDPublicKey._validateBufferArguments(arg); this._buffers = arg; @@ -280,10 +246,12 @@ HDPublicKey.prototype._buildFromBuffers = function (arg) { arg.version, arg.depth, arg.parentFingerPrint, arg.childIndex, arg.chainCode, arg.publicKey ]; + var concat = buffer.Buffer.concat(sequence); + var checksum = Base58Check.checksum(concat); if (!arg.checksum || !arg.checksum.length) { - arg.checksum = Base58Check.checksum(buffer.Buffer.concat(sequence)); + arg.checksum = checksum; } else { - if (arg.checksum.toString() !== sequence.toString()) { + if (arg.checksum.toString('hex') !== checksum.toString('hex')) { throw new Error(HDPublicKey.Errors.InvalidB58Checksum); } } @@ -328,7 +296,7 @@ HDPublicKey.prototype.toString = function () { HDPublicKey.prototype.toObject = function () { return { - network: Network.get(util.integerFromBuffer(this._buffers.version)), + network: Network.get(util.integerFromBuffer(this._buffers.version)).name, depth: util.integerFromSingleByteBuffer(this._buffers.depth), fingerPrint: util.integerFromBuffer(this.fingerPrint), parentFingerPrint: util.integerFromBuffer(this._buffers.parentFingerPrint), @@ -344,12 +312,8 @@ HDPublicKey.prototype.toJson = function () { return JSON.stringify(this.toObject()); }; -HDPublicKey.DefaultDepth = 0; -HDPublicKey.DefaultFingerprint = 0; -HDPublicKey.DefaultChildIndex = 0; -HDPublicKey.DefaultNetwork = Network.livenet; HDPublicKey.Hardened = 0x80000000; -HDPublicKey.RootElementAlias = ['m', 'M', 'm\'', 'M\'']; +HDPublicKey.RootElementAlias = ['m', 'M']; HDPublicKey.VersionSize = 4; HDPublicKey.DepthSize = 1; @@ -393,6 +357,7 @@ HDPublicKey.Errors.InvalidNetwork = 'Unexpected version for network'; HDPublicKey.Errors.InvalidNetworkArgument = 'Network argument must be \'livenet\' or \'testnet\''; HDPublicKey.Errors.InvalidParentFingerPrint = 'Invalid Parent Fingerprint - must be a number'; HDPublicKey.Errors.InvalidPath = 'Invalid path for derivation: must start with "m"'; +HDPublicKey.Errors.MustSupplyArgument = 'Must supply an argument for the constructor'; HDPublicKey.Errors.UnrecognizedArgument = 'Creating a HDPublicKey requires a string, a buffer, a json, or an object'; module.exports = HDPublicKey; diff --git a/test/hdprivatekey.js b/test/hdprivatekey.js index 8b233d647..bba1d9c8c 100644 --- a/test/hdprivatekey.js +++ b/test/hdprivatekey.js @@ -40,7 +40,7 @@ describe('HDPrivate key interface', function() { regenerate.xprivkey.should.equal(xprivkey); }); - it('builds a json keeping the same interface than previous versions', function() { + it('builds a json keeping the structure and same members', function() { assert(util.shallowEquals( JSON.parse(new HDPrivateKey(json).toJson()), JSON.parse(new HDPrivateKey(xprivkey).toJson()) diff --git a/test/hdpublickey.js b/test/hdpublickey.js new file mode 100644 index 000000000..2c208a653 --- /dev/null +++ b/test/hdpublickey.js @@ -0,0 +1,153 @@ +'use strict'; + +/* jshint unused: false */ +var _ = require('lodash'); +var assert = require('assert'); +var should = require('chai').should(); +var expect = require('chai').expect; +var bitcore = require('..'); +var buffer = require('buffer'); +var util = bitcore.util; +var HDPrivateKey = bitcore.HDPrivateKey; +var HDPublicKey = bitcore.HDPublicKey; +var Base58Check = bitcore.encoding.Base58Check; + +var xprivkey = 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'; +var xpubkey = 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; +var json = '{"network":"livenet","depth":0,"fingerPrint":876747070,"parentFingerPrint":0,"childIndex":0,"chainCode":"873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508","publicKey":"0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2","checksum":-1421395167,"xpubkey":"xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"}'; +var derived_0_1_200000 = 'xpub6BqyndF6rkBNTV6LXwiY8Pco8aqctqq7tGEUdA8fmGDTnDJphn2fmxr3eM8Lm3m8TrNUsLbEjHvpa3adBU18YpEx4tp2Zp6nqax3mQkudhX'; + +describe('HDPublicKey interface', function() { + + var expectFail = function(argument, error) { + return function() { + expect(function() { + return new HDPublicKey(argument); + }).to.throw(error); + }; + }; + describe('creation formats', function() { + + it('returns same argument if already an instance of HDPublicKey', function() { + var publicKey = new HDPublicKey(xpubkey); + publicKey.should.equal(new HDPublicKey(publicKey)); + }); + + it('returns the correct xpubkey for a xprivkey', function() { + var publicKey = new HDPublicKey(xprivkey); + publicKey.xpubkey.should.equal(xpubkey); + }); + + it('allows to call the argument with no "new" keyword', function() { + HDPublicKey(xpubkey).xpubkey.should.equal(new HDPublicKey(xpubkey).xpubkey); + }); + + it('fails when user doesn\'t supply an argument', function() { + expect(function() { return new HDPublicKey(); }).to.throw(HDPublicKey.Errors.MustSupplyArgument); + }); + + it('doesn\'t recognize an invalid argument', function() { + var expectCreationFail = function(argument) { + expect(function() { return new HDPublicKey(argument); }).to.throw(HDPublicKey.Errors.UnrecognizedArgument); + }; + expectCreationFail(1); + expectCreationFail(true); + }); + + + describe('xpubkey string serialization errors', function() { + it('fails on invalid length', expectFail( + Base58Check.encode(new buffer.Buffer([1, 2, 3])), + HDPublicKey.Errors.InvalidLength + )); + it('fails on invalid base58 encoding', expectFail( + xpubkey + '1', + HDPublicKey.Errors.InvalidB58Checksum + )); + }); + + it('can be generated from a json', function() { + expect(new HDPublicKey(json).xpubkey).to.equal(xpubkey); + }); + + it('can generate a json that has a particular structure', function() { + assert(util.shallowEquals( + JSON.parse(new HDPublicKey(json).toJson()), + JSON.parse(new HDPublicKey(xpubkey).toJson()) + )); + }); + + it('builds from a buffer object', function() { + (new HDPublicKey(new HDPublicKey(xpubkey)._buffers)).xpubkey.should.equal(xpubkey); + }); + + it('checks the checksum', function() { + var buffers = new HDPublicKey(xpubkey)._buffers; + buffers.checksum = util.integerAsBuffer(1); + expectFail(buffers, HDPublicKey.Errors.InvalidB58Checksum)(); + }); + + }); + + describe('error checking on serialization', function() { + it('throws invalid argument when argument is not a string or buffer', function() { + HDPublicKey.getSerializedError(1).should.equal(HDPublicKey.Errors.InvalidArgument); + }); + it('if a network is provided, validates that data corresponds to it', function() { + HDPublicKey.getSerializedError(xpubkey, 'testnet').should.equal(HDPublicKey.Errors.InvalidNetwork); + }); + it('recognizes invalid network arguments', function() { + HDPublicKey.getSerializedError(xpubkey, 'invalid').should.equal(HDPublicKey.Errors.InvalidNetworkArgument); + }); + it('recognizes a valid network', function() { + expect(HDPublicKey.getSerializedError(xpubkey, 'livenet')).to.equal(null); + }); + }); + + it('toString() returns the same value as .xpubkey', function() { + var pubKey = new HDPublicKey(xpubkey); + pubKey.toString().should.equal(pubKey.xpubkey); + }); + + describe('derivation', function() { + it('derivation is the same whether deriving with number or string', function() { + var pubkey = new HDPublicKey(xpubkey); + var derived1 = pubkey.derive(0).derive(1).derive(200000); + var derived2 = pubkey.derive('m/0/1/200000'); + derived1.xpubkey.should.equal(derived_0_1_200000); + derived2.xpubkey.should.equal(derived_0_1_200000); + }); + + it('allows special parameters m, M', function() { + var expectDerivationSuccess = function(argument) { + new HDPublicKey(xpubkey).derive(argument).xpubkey.should.equal(xpubkey); + }; + expectDerivationSuccess('m'); + expectDerivationSuccess('M'); + }); + + it('doesn\'t allow object arguments for derivation', function() { + expect(function() { + return new HDPublicKey(xpubkey).derive({}); + }).to.throw(HDPublicKey.Errors.InvalidDerivationArgument); + }); + + it('doesn\'t allow other parameters like m\' or M\' or "s"', function() { + var expectDerivationFail = function(argument) { + expect(function() { + return new HDPublicKey(xpubkey).derive(argument); + }).to.throw(HDPublicKey.Errors.InvalidPath); + }; + /* jshint quotmark: double */ + expectDerivationFail("m'"); + expectDerivationFail("M'"); + expectDerivationFail("1"); + expectDerivationFail("S"); + }); + + it('can\'t derive hardened keys', function() { + expect(function() { return new HDPublicKey(xpubkey).derive(HDPublicKey.Hardened + 1); }) + .to.throw(HDPublicKey.Errors.InvalidIndexCantDeriveHardened); + }); + }); +});