diff --git a/lib/hdprivatekey.js b/lib/hdprivatekey.js index 8d2d222..b8409f8 100644 --- a/lib/hdprivatekey.js +++ b/lib/hdprivatekey.js @@ -69,15 +69,51 @@ function HDPrivateKey(arg) { * @param {boolean?} hardened * @return {boolean} */ -HDPrivateKey.prototype.isValidPath = function(arg, hardened) { - try { - this.derive(arg, hardened); - return true; - } catch (err) { - return false; +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 validates if a 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. * @@ -114,18 +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 (index < HDPrivateKey.Hardened && hardened) { - index += HDPrivateKey.Hardened; - } - - if (index < 0 || index >= HDPrivateKey.MaxIndex) { + if (!HDPrivateKey.isValidPath(index, hardened)) { throw new hdErrors.InvalidPath(index); } + 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; @@ -157,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; }; /** diff --git a/test/hdprivatekey.js b/test/hdprivatekey.js index 4e7688b..6191743 100644 --- a/test/hdprivatekey.js +++ b/test/hdprivatekey.js @@ -190,22 +190,70 @@ describe('HDPrivate key interface', function() { derivedByNumber.xprivkey.should.equal(derivedByString.xprivkey); }); - it('validates correct paths', function() { - var privateKey = new HDPrivateKey(xprivkey); - var valid = privateKey.isValidPath('m/0\'/1/2\''); - valid.should.equal(true); + describe('validates paths', function() { + it('validates correct paths', function() { + var valid; - var valid = privateKey.isValidPath(123, true); - valid.should.equal(true); - }); + valid = HDPrivateKey.isValidPath("m/0'/1/2'"); + valid.should.equal(true); - it('validates illegal paths', function() { - var privateKey = new HDPrivateKey(xprivkey); - var valid = privateKey.isValidPath('m/-1/12'); - valid.should.equal(false); + valid = HDPrivateKey.isValidPath('m'); + valid.should.equal(true); - var valid = privateKey.isValidPath(HDPrivateKey.MaxHardened); - valid.should.equal(false); + 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() {