Merge pull request #889 from yemel/fix/add-derivation-check
Add method for validating a derivation path
This commit is contained in:
commit
9ba7eff9e0
|
@ -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.
|
* 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) {
|
HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) {
|
||||||
/* jshint maxstatements: 20 */
|
/* jshint maxstatements: 20 */
|
||||||
/* jshint maxcomplexity: 10 */
|
/* jshint maxcomplexity: 10 */
|
||||||
if (index >= HDPrivateKey.Hardened) {
|
if (!HDPrivateKey.isValidPath(index, hardened)) {
|
||||||
hardened = true;
|
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;
|
index += HDPrivateKey.Hardened;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cached = HDKeyCache.get(this.xprivkey, index, hardened);
|
var cached = HDKeyCache.get(this.xprivkey, index, hardened);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
|
@ -135,24 +190,16 @@ HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) {
|
||||||
};
|
};
|
||||||
|
|
||||||
HDPrivateKey.prototype._deriveFromString = function(path) {
|
HDPrivateKey.prototype._deriveFromString = function(path) {
|
||||||
var steps = path.split('/');
|
if (!HDPrivateKey.isValidPath(path)) {
|
||||||
|
|
||||||
// Special cases:
|
|
||||||
if (_.contains(HDPrivateKey.RootElementAlias, path)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (!_.contains(HDPrivateKey.RootElementAlias, steps[0])) {
|
|
||||||
throw new hdErrors.InvalidPath(path);
|
throw new hdErrors.InvalidPath(path);
|
||||||
}
|
}
|
||||||
steps = steps.slice(1);
|
|
||||||
|
|
||||||
var result = this;
|
var indexes = HDPrivateKey._getDerivationIndexes(path);
|
||||||
for (var step in steps) {
|
var derived = indexes.reduce(function(prev, index) {
|
||||||
var index = parseInt(steps[step]);
|
return prev._deriveWithNumber(index);
|
||||||
var hardened = steps[step] !== index.toString();
|
}, this);
|
||||||
result = result._deriveWithNumber(index, hardened);
|
|
||||||
}
|
return derived;
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -441,6 +488,8 @@ HDPrivateKey.DefaultDepth = 0;
|
||||||
HDPrivateKey.DefaultFingerprint = 0;
|
HDPrivateKey.DefaultFingerprint = 0;
|
||||||
HDPrivateKey.DefaultChildIndex = 0;
|
HDPrivateKey.DefaultChildIndex = 0;
|
||||||
HDPrivateKey.Hardened = 0x80000000;
|
HDPrivateKey.Hardened = 0x80000000;
|
||||||
|
HDPrivateKey.MaxIndex = 2 * HDPrivateKey.Hardened;
|
||||||
|
|
||||||
HDPrivateKey.RootElementAlias = ['m', 'M', 'm\'', 'M\''];
|
HDPrivateKey.RootElementAlias = ['m', 'M', 'm\'', 'M\''];
|
||||||
|
|
||||||
HDPrivateKey.VersionSize = 4;
|
HDPrivateKey.VersionSize = 4;
|
||||||
|
|
|
@ -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.
|
* Get a derivated child based on a string or number.
|
||||||
*
|
*
|
||||||
|
@ -86,11 +105,10 @@ function HDPublicKey(arg) {
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param {string|number} arg
|
* @param {string|number} arg
|
||||||
* @param {boolean?} hardened
|
|
||||||
*/
|
*/
|
||||||
HDPublicKey.prototype.derive = function (arg, hardened) {
|
HDPublicKey.prototype.derive = function (arg) {
|
||||||
if (_.isNumber(arg)) {
|
if (_.isNumber(arg)) {
|
||||||
return this._deriveWithNumber(arg, hardened);
|
return this._deriveWithNumber(arg);
|
||||||
} else if (_.isString(arg)) {
|
} else if (_.isString(arg)) {
|
||||||
return this._deriveFromString(arg);
|
return this._deriveFromString(arg);
|
||||||
} else {
|
} else {
|
||||||
|
@ -98,11 +116,14 @@ HDPublicKey.prototype.derive = function (arg, hardened) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
HDPublicKey.prototype._deriveWithNumber = function (index, hardened) {
|
HDPublicKey.prototype._deriveWithNumber = function (index) {
|
||||||
if (hardened || index >= HDPublicKey.Hardened) {
|
if (index >= HDPublicKey.Hardened) {
|
||||||
throw new hdErrors.InvalidIndexCantDeriveHardened();
|
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) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
@ -123,30 +144,24 @@ HDPublicKey.prototype._deriveWithNumber = function (index, hardened) {
|
||||||
chainCode: chainCode,
|
chainCode: chainCode,
|
||||||
publicKey: publicKey
|
publicKey: publicKey
|
||||||
});
|
});
|
||||||
HDKeyCache.set(this.xpubkey, index, hardened, derived);
|
HDKeyCache.set(this.xpubkey, index, false, derived);
|
||||||
return derived;
|
return derived;
|
||||||
};
|
};
|
||||||
|
|
||||||
HDPublicKey.prototype._deriveFromString = function (path) {
|
HDPublicKey.prototype._deriveFromString = function (path) {
|
||||||
/* jshint maxcomplexity: 8 */
|
/* jshint maxcomplexity: 8 */
|
||||||
var steps = path.split('/');
|
if (_.contains(path, "'")) {
|
||||||
|
throw new hdErrors.InvalidIndexCantDeriveHardened();
|
||||||
// Special cases:
|
} else if (!HDPublicKey.isValidPath(path)) {
|
||||||
if (_.contains(HDPublicKey.RootElementAlias, path)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (!_.contains(HDPublicKey.RootElementAlias, steps[0])) {
|
|
||||||
throw new hdErrors.InvalidPath(path);
|
throw new hdErrors.InvalidPath(path);
|
||||||
}
|
}
|
||||||
steps = steps.slice(1);
|
|
||||||
|
|
||||||
var result = this;
|
var indexes = HDPrivateKey._getDerivationIndexes(path);
|
||||||
for (var step in steps) {
|
var derived = indexes.reduce(function(prev, index) {
|
||||||
var index = parseInt(steps[step]);
|
return prev._deriveWithNumber(index);
|
||||||
var hardened = steps[step] !== index.toString();
|
}, this);
|
||||||
result = result._deriveWithNumber(index, hardened);
|
|
||||||
}
|
return derived;
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -196,6 +196,72 @@ describe('HDPrivate key interface', function() {
|
||||||
derivedByNumber.xprivkey.should.equal(derivedByString.xprivkey);
|
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() {
|
describe('conversion to plain object/json', function() {
|
||||||
var plainObject = {
|
var plainObject = {
|
||||||
'network':'livenet',
|
'network':'livenet',
|
||||||
|
|
|
@ -216,10 +216,48 @@ describe('HDPublicKey interface', function() {
|
||||||
|
|
||||||
it('can\'t derive hardened keys', function() {
|
it('can\'t derive hardened keys', function() {
|
||||||
expectFail(function() {
|
expectFail(function() {
|
||||||
return new HDPublicKey(xpubkey).derive(HDPublicKey.Hardened + 1);
|
return new HDPublicKey(xpubkey).derive(HDPublicKey.Hardened);
|
||||||
}, hdErrors.InvalidDerivationArgument);
|
}, 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() {
|
it('should use the cache', function() {
|
||||||
var pubkey = new HDPublicKey(xpubkey);
|
var pubkey = new HDPublicKey(xpubkey);
|
||||||
var derived1 = pubkey.derive(0);
|
var derived1 = pubkey.derive(0);
|
||||||
|
|
Loading…
Reference in New Issue