diff --git a/lib/hdprivatekey.js b/lib/hdprivatekey.js index c3f85a5..3026aa4 100644 --- a/lib/hdprivatekey.js +++ b/lib/hdprivatekey.js @@ -62,6 +62,58 @@ function HDPrivateKey(arg) { } } +/** + * Verifies that a given path is valid. + * + * @param {string|number} arg + * @param {boolean?} hardened + * @return {boolean} + */ +HDPrivateKey.isValidPath = function(arg, hardened) { + if (_.isString(arg)) { + var indexes = HDPrivateKey._getDerivationIndexes(arg); + return indexes !== null && _.all(indexes, HDPrivateKey.isValidPath); + } + + if (_.isNumber(arg)) { + if (arg < HDPrivateKey.Hardened && hardened === true) { + arg += HDPrivateKey.Hardened; + } + return arg >= 0 && arg < HDPrivateKey.MaxIndex; + } + + return false; +}; + +/** + * Internal function that splits a string path into a derivation index array. + * It will return null if the string path is malformed. + * It does not validate if indexes are in bounds. + * + * @param {string} path + * @return {Array} + */ +HDPrivateKey._getDerivationIndexes = function(path) { + var steps = path.split('/'); + + // Special cases: + if (_.contains(HDPrivateKey.RootElementAlias, path)) { + return []; + } + + if (!_.contains(HDPrivateKey.RootElementAlias, steps[0])) { + return null; + } + + var indexes = steps.slice(1).map(function(step) { + var index = parseInt(step); + index += step != index.toString() ? HDPrivateKey.Hardened : 0; + return index; + }); + + return _.any(indexes, isNaN) ? null : indexes; +} + /** * Get a derivated child based on a string or number. * @@ -98,12 +150,15 @@ HDPrivateKey.prototype.derive = function(arg, hardened) { HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) { /* jshint maxstatements: 20 */ /* jshint maxcomplexity: 10 */ - if (index >= HDPrivateKey.Hardened) { - hardened = true; + if (!HDPrivateKey.isValidPath(index, hardened)) { + throw new hdErrors.InvalidPath(index); } - if (index < HDPrivateKey.Hardened && hardened) { + + hardened = index >= HDPrivateKey.Hardened ? true : hardened; + if (index < HDPrivateKey.Hardened && hardened === true) { index += HDPrivateKey.Hardened; } + var cached = HDKeyCache.get(this.xprivkey, index, hardened); if (cached) { return cached; @@ -135,24 +190,16 @@ HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) { }; HDPrivateKey.prototype._deriveFromString = function(path) { - var steps = path.split('/'); - - // Special cases: - if (_.contains(HDPrivateKey.RootElementAlias, path)) { - return this; - } - if (!_.contains(HDPrivateKey.RootElementAlias, steps[0])) { + if (!HDPrivateKey.isValidPath(path)) { throw new hdErrors.InvalidPath(path); } - steps = steps.slice(1); - var result = this; - for (var step in steps) { - var index = parseInt(steps[step]); - var hardened = steps[step] !== index.toString(); - result = result._deriveWithNumber(index, hardened); - } - return result; + var indexes = HDPrivateKey._getDerivationIndexes(path); + var derived = indexes.reduce(function(prev, index) { + return prev._deriveWithNumber(index); + }, this); + + return derived; }; /** @@ -441,6 +488,8 @@ HDPrivateKey.DefaultDepth = 0; HDPrivateKey.DefaultFingerprint = 0; HDPrivateKey.DefaultChildIndex = 0; HDPrivateKey.Hardened = 0x80000000; +HDPrivateKey.MaxIndex = 2 * HDPrivateKey.Hardened; + HDPrivateKey.RootElementAlias = ['m', 'M', 'm\'', 'M\'']; HDPrivateKey.VersionSize = 4; diff --git a/lib/hdpublickey.js b/lib/hdpublickey.js index 95bd8e2..323b71c 100644 --- a/lib/hdpublickey.js +++ b/lib/hdpublickey.js @@ -65,6 +65,25 @@ function HDPublicKey(arg) { } } +/** + * Verifies that a given path is valid. + * + * @param {string|number} arg + * @return {boolean} + */ +HDPublicKey.isValidPath = function(arg) { + if (_.isString(arg)) { + var indexes = HDPrivateKey._getDerivationIndexes(arg); + return indexes !== null && _.all(indexes, HDPublicKey.isValidPath); + } + + if (_.isNumber(arg)) { + return arg >= 0 && arg < HDPublicKey.Hardened; + } + + return false; +}; + /** * Get a derivated child based on a string or number. * @@ -86,11 +105,10 @@ function HDPublicKey(arg) { * ``` * * @param {string|number} arg - * @param {boolean?} hardened */ -HDPublicKey.prototype.derive = function (arg, hardened) { +HDPublicKey.prototype.derive = function (arg) { if (_.isNumber(arg)) { - return this._deriveWithNumber(arg, hardened); + return this._deriveWithNumber(arg); } else if (_.isString(arg)) { return this._deriveFromString(arg); } else { @@ -98,11 +116,14 @@ HDPublicKey.prototype.derive = function (arg, hardened) { } }; -HDPublicKey.prototype._deriveWithNumber = function (index, hardened) { - if (hardened || index >= HDPublicKey.Hardened) { +HDPublicKey.prototype._deriveWithNumber = function (index) { + if (index >= HDPublicKey.Hardened) { throw new hdErrors.InvalidIndexCantDeriveHardened(); } - var cached = HDKeyCache.get(this.xpubkey, index, hardened); + if (index < 0) { + throw new hdErrors.InvalidPath(index); + } + var cached = HDKeyCache.get(this.xpubkey, index, false); if (cached) { return cached; } @@ -123,30 +144,24 @@ HDPublicKey.prototype._deriveWithNumber = function (index, hardened) { chainCode: chainCode, publicKey: publicKey }); - HDKeyCache.set(this.xpubkey, index, hardened, derived); + HDKeyCache.set(this.xpubkey, index, false, derived); return derived; }; HDPublicKey.prototype._deriveFromString = function (path) { /* jshint maxcomplexity: 8 */ - var steps = path.split('/'); - - // Special cases: - if (_.contains(HDPublicKey.RootElementAlias, path)) { - return this; - } - if (!_.contains(HDPublicKey.RootElementAlias, steps[0])) { + if (_.contains(path, "'")) { + throw new hdErrors.InvalidIndexCantDeriveHardened(); + } else if (!HDPublicKey.isValidPath(path)) { throw new hdErrors.InvalidPath(path); } - steps = steps.slice(1); - var result = this; - for (var step in steps) { - var index = parseInt(steps[step]); - var hardened = steps[step] !== index.toString(); - result = result._deriveWithNumber(index, hardened); - } - return result; + var indexes = HDPrivateKey._getDerivationIndexes(path); + var derived = indexes.reduce(function(prev, index) { + return prev._deriveWithNumber(index); + }, this); + + return derived; }; /** diff --git a/test/hdprivatekey.js b/test/hdprivatekey.js index d8779c7..e6c57e8 100644 --- a/test/hdprivatekey.js +++ b/test/hdprivatekey.js @@ -196,6 +196,72 @@ describe('HDPrivate key interface', function() { derivedByNumber.xprivkey.should.equal(derivedByString.xprivkey); }); + describe('validates paths', function() { + it('validates correct paths', function() { + var valid; + + valid = HDPrivateKey.isValidPath("m/0'/1/2'"); + valid.should.equal(true); + + valid = HDPrivateKey.isValidPath('m'); + valid.should.equal(true); + + valid = HDPrivateKey.isValidPath(123, true); + valid.should.equal(true); + + valid = HDPrivateKey.isValidPath(123); + valid.should.equal(true); + + valid = HDPrivateKey.isValidPath(HDPrivateKey.Hardened + 123); + valid.should.equal(true); + + valid = HDPrivateKey.isValidPath(HDPrivateKey.Hardened + 123, true); + valid.should.equal(true); + }); + + it('rejects illegal paths', function() { + var valid; + + valid = HDPrivateKey.isValidPath('m/-1/12'); + valid.should.equal(false); + + valid = HDPrivateKey.isValidPath('bad path'); + valid.should.equal(false); + + valid = HDPrivateKey.isValidPath('K'); + valid.should.equal(false); + + valid = HDPrivateKey.isValidPath('m/'); + valid.should.equal(false); + + valid = HDPrivateKey.isValidPath(HDPrivateKey.MaxHardened); + valid.should.equal(false); + }); + + it('generates deriving indexes correctly', function() { + var indexes; + + indexes = HDPrivateKey._getDerivationIndexes('m/-1/12'); + indexes.should.eql([-1, 12]); + + indexes = HDPrivateKey._getDerivationIndexes("m/0/12/12'"); + indexes.should.eql([0, 12, HDPrivateKey.Hardened + 12]); + + indexes = HDPrivateKey._getDerivationIndexes("m/0/12/12'"); + indexes.should.eql([0, 12, HDPrivateKey.Hardened + 12]); + }); + + it('rejects invalid derivation path', function() { + var indexes; + + indexes = HDPrivateKey._getDerivationIndexes("m/"); + expect(indexes).to.be.null; + + indexes = HDPrivateKey._getDerivationIndexes("bad path"); + expect(indexes).to.be.null; + }); + }); + describe('conversion to plain object/json', function() { var plainObject = { 'network':'livenet', diff --git a/test/hdpublickey.js b/test/hdpublickey.js index 721fca8..f67b1e4 100644 --- a/test/hdpublickey.js +++ b/test/hdpublickey.js @@ -216,10 +216,48 @@ describe('HDPublicKey interface', function() { it('can\'t derive hardened keys', function() { expectFail(function() { - return new HDPublicKey(xpubkey).derive(HDPublicKey.Hardened + 1); + return new HDPublicKey(xpubkey).derive(HDPublicKey.Hardened); }, hdErrors.InvalidDerivationArgument); }); + it('validates correct paths', function() { + var valid; + + valid = HDPublicKey.isValidPath('m/123/12'); + valid.should.equal(true); + + valid = HDPublicKey.isValidPath('m'); + valid.should.equal(true); + + valid = HDPublicKey.isValidPath(123); + valid.should.equal(true); + }); + + it('rejects illegal paths', function() { + var valid; + + valid = HDPublicKey.isValidPath('m/-1/12'); + valid.should.equal(false); + + valid = HDPublicKey.isValidPath("m/0'/12"); + valid.should.equal(false); + + valid = HDPublicKey.isValidPath("m/8000000000/12"); + valid.should.equal(false); + + valid = HDPublicKey.isValidPath('bad path'); + valid.should.equal(false); + + valid = HDPublicKey.isValidPath(-1); + valid.should.equal(false); + + valid = HDPublicKey.isValidPath(8000000000); + valid.should.equal(false); + + valid = HDPublicKey.isValidPath(HDPublicKey.Hardened); + valid.should.equal(false); + }); + it('should use the cache', function() { var pubkey = new HDPublicKey(xpubkey); var derived1 = pubkey.derive(0);