-
Notifications
You must be signed in to change notification settings - Fork 1k
Fix implementation of hd derivation to be bip32 compliant #97
Changes from all commits
b79a9b2
0906d98
d0e3f84
e9d1237
d32ae41
4760c90
fb606ea
57b3d95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,17 +8,17 @@ module.exports = { | |
| _usedIndex: {}, | ||
| _CACHE_SIZE: 5000, | ||
|
|
||
| get: function(xkey, number, hardened) { | ||
| get: function(xkey, number, hardened, nonCompliant) { | ||
| hardened = !!hardened; | ||
| var key = xkey + '/' + number + '/' + hardened; | ||
| var key = (nonCompliant ? '1' : '0') + xkey + '/' + number + '/' + hardened; | ||
| if (this._cache[key]) { | ||
| this._cacheHit(key); | ||
| return this._cache[key]; | ||
| } | ||
| }, | ||
| set: function(xkey, number, hardened, derived) { | ||
| set: function(xkey, number, hardened, derived, nonCompliant) { | ||
| hardened = !!hardened; | ||
| var key = xkey + '/' + number + '/' + hardened; | ||
| var key = (nonCompliant ? '1' : '0') + xkey + '/' + number + '/' + hardened; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. This should be commented. See also http://butunclebob.com/ArticleS.TimOttinger.ApologizeIncode |
||
| this._cache[key] = derived; | ||
| this._cacheHit(key); | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,6 +127,11 @@ HDPrivateKey._getDerivationIndexes = function(path) { | |
| return _.any(indexes, isNaN) ? null : indexes; | ||
| }; | ||
|
|
||
| HDPrivateKey.prototype.derive = function() { | ||
| throw new Error('Method has been deprecated, please use deriveChild or deriveNonCompliantChild' + | ||
| 'and see documentation for more information'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this really what one would expect? Why not the remove the function altogether? Also makes the code better readible in the future.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to make sure that if someone upgrades to 1.0.0, that they will not mistakenly be unaware of the bug that affected the deprecated function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I unterstand that. So just completely remove the method. This will also throw an error of someone wants to use it, but then it does not show in IDE autocomplete and so on. Just think how annoying this method will be in the future. |
||
| }; | ||
|
|
||
| /** | ||
| * Get a derived child based on a string or number. | ||
| * | ||
|
|
@@ -142,25 +147,48 @@ HDPrivateKey._getDerivationIndexes = function(path) { | |
| * @example | ||
| * ```javascript | ||
| * var parent = new HDPrivateKey('xprv...'); | ||
| * var child_0_1_2h = parent.derive(0).derive(1).derive(2, true); | ||
| * var copy_of_child_0_1_2h = parent.derive("m/0/1/2'"); | ||
| * var child_0_1_2h = parent.deriveChild(0).deriveChild(1).deriveChild(2, true); | ||
| * var copy_of_child_0_1_2h = parent.deriveChild("m/0/1/2'"); | ||
| * assert(child_0_1_2h.xprivkey === copy_of_child_0_1_2h); | ||
| * ``` | ||
| * | ||
| * @param {string|number} arg | ||
| * @param {boolean?} hardened | ||
| */ | ||
| HDPrivateKey.prototype.derive = function(arg, hardened) { | ||
| HDPrivateKey.prototype.deriveChild = function(arg, hardened) { | ||
| var derived = this._derive(arg, hardened, false); | ||
| return derived; | ||
| }; | ||
|
|
||
| /** | ||
| * WARNING: If this is a new implementation you should NOT use this method, you should be using | ||
| * `derive` instead. | ||
| * | ||
| * This method is explicitly for use and compatibility with an implementation that | ||
| * was not compliant with BIP32 regarding the derivation algorithm. The private key | ||
| * must be 32 bytes hashing, and this implementation will use the non-zero padded | ||
| * serialization of a private key, such that it's still possible to derive the privateKey | ||
| * to recover those funds. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As much as I like this comment, it is easy to overlook it. One should log a warning when someone is using this function too.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like the method name |
||
| * | ||
| * @param {string|number} arg | ||
| * @param {boolean?} hardened | ||
| */ | ||
| HDPrivateKey.prototype.deriveNonCompliantChild = function(arg, hardened) { | ||
| var derived = this._derive(arg, hardened, true); | ||
| return derived; | ||
| }; | ||
|
|
||
| HDPrivateKey.prototype._derive = function(arg, hardened, nonCompliant) { | ||
| if (_.isNumber(arg)) { | ||
| return this._deriveWithNumber(arg, hardened); | ||
| return this._deriveWithNumber(arg, hardened, nonCompliant); | ||
| } else if (_.isString(arg)) { | ||
| return this._deriveFromString(arg); | ||
| return this._deriveFromString(arg, nonCompliant); | ||
| } else { | ||
| throw new hdErrors.InvalidDerivationArgument(arg); | ||
| } | ||
| }; | ||
|
|
||
| HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) { | ||
| HDPrivateKey.prototype._deriveWithNumber = function(index, hardened, nonCompliant) { | ||
| /* jshint maxstatements: 20 */ | ||
| /* jshint maxcomplexity: 10 */ | ||
| if (!HDPrivateKey.isValidPath(index, hardened)) { | ||
|
|
@@ -172,15 +200,23 @@ HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) { | |
| index += HDPrivateKey.Hardened; | ||
| } | ||
|
|
||
| var cached = HDKeyCache.get(this.xprivkey, index, hardened); | ||
| var cached = HDKeyCache.get(this.xprivkey, index, hardened, nonCompliant); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. Do we really want to drag this "nonCompliant" parameter through the ages from now on? |
||
| if (cached) { | ||
| return cached; | ||
| } | ||
|
|
||
| var indexBuffer = BufferUtil.integerAsBuffer(index); | ||
| var data; | ||
| if (hardened) { | ||
| data = BufferUtil.concat([new buffer.Buffer([0]), this.privateKey.toBuffer(), indexBuffer]); | ||
| if (hardened && nonCompliant) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if it is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nonCompliant derivation (bug) only affects hardened derivations. |
||
| // The private key serialization in this case will not be exactly 32 bytes and can be | ||
| // any value less, and the value is not zero-padded. | ||
| var nonZeroPadded = this.privateKey.bn.toBuffer(); | ||
| data = BufferUtil.concat([new buffer.Buffer([0]), nonZeroPadded, indexBuffer]); | ||
| } else if (hardened) { | ||
| // This will use a 32 byte zero padded serialization of the private key | ||
| var privateKeyBuffer = this.privateKey.bn.toBuffer({size: 32}); | ||
| assert(privateKeyBuffer.length === 32, 'length of private key buffer is expected to be 32 bytes'); | ||
| data = BufferUtil.concat([new buffer.Buffer([0]), privateKeyBuffer, indexBuffer]); | ||
| } else { | ||
| data = BufferUtil.concat([this.publicKey.toBuffer(), indexBuffer]); | ||
| } | ||
|
|
@@ -194,6 +230,11 @@ HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) { | |
| size: 32 | ||
| }); | ||
|
|
||
| if (!PrivateKey.isValid(privateKey)) { | ||
| // Index at this point is already hardened, we can pass null as the hardened arg | ||
| return this._deriveWithNumber(index + 1, null, nonCompliant); | ||
| } | ||
|
|
||
| var derived = new HDPrivateKey({ | ||
| network: this.network, | ||
| depth: this.depth + 1, | ||
|
|
@@ -202,18 +243,18 @@ HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) { | |
| chainCode: chainCode, | ||
| privateKey: privateKey | ||
| }); | ||
| HDKeyCache.set(this.xprivkey, index, hardened, derived); | ||
| HDKeyCache.set(this.xprivkey, index, hardened, derived, nonCompliant); | ||
| return derived; | ||
| }; | ||
|
|
||
| HDPrivateKey.prototype._deriveFromString = function(path) { | ||
| HDPrivateKey.prototype._deriveFromString = function(path, nonCompliant) { | ||
| if (!HDPrivateKey.isValidPath(path)) { | ||
| throw new hdErrors.InvalidPath(path); | ||
| } | ||
|
|
||
| var indexes = HDPrivateKey._getDerivationIndexes(path); | ||
| var derived = indexes.reduce(function(prev, index) { | ||
| return prev._deriveWithNumber(index); | ||
| return prev._deriveWithNumber(index, null, nonCompliant); | ||
| }, this); | ||
|
|
||
| return derived; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should a comment in the code here. Nobody will get what and why this is done.