diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index bcac4a1a..8bb6c13a 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -87,7 +87,7 @@ var KeyPair = bcoin.keypair; var LRU = require('./lru'); var BufferWriter = require('./writer'); var BufferReader = require('./reader'); -var unorm = require('../../vendor/unorm'); +var unorm; /** * HD Mnemonic @@ -137,8 +137,8 @@ Mnemonic.prototype.toSeed = function toSeed() { this.phrase = this.createMnemonic(); this.seed = utils.pbkdf2( - unorm.nfkd(this.phrase), - unorm.nfkd('mnemonic' + this.passphrase), + nfkd(this.phrase), + nfkd('mnemonic' + this.passphrase), 2048, 64); return this.seed; @@ -337,6 +337,53 @@ HD.isExtended = function isExtended(data) { || HDPublicKey.isExtended(data); }; +/** + * Parse a derivation path and return an array of indexes. + * @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki + * @param {String} path + * @param {Number?} max - Max index. + * @returns {Number[]} + */ + +HD.parsePath = function parsePath(path, max) { + var parts = path.split('/'); + var root = parts.shift(); + var indexes = []; + var i, hardened, index; + + if (max == null) + max = constants.hd.MAX_INDEX; + + if (constants.hd.PATH_ROOTS.indexOf(path) !== -1) + return indexes; + + if (constants.hd.PATH_ROOTS.indexOf(root) === -1) + throw new Error('Bad path root.'); + + for (i = 0; i < parts.length; i++) { + index = parts[i]; + hardened = index[index.length - 1] === '\''; + + if (hardened) + index = index.slice(0, -1); + + index = +index; + + if (!(index >= 0)) + throw new Error('Non-number path index.'); + + if (hardened) + index += constants.hd.HARDENED; + + if (!(index >= 0 && index < max)) + throw new Error('Index out of range.'); + + indexes.push(index); + } + + return indexes; +}; + /** * LRU cache to avoid deriving keys twice. * @type {LRU} @@ -594,45 +641,6 @@ HDPrivateKey.isExtended = function isExtended(data) { return false; }; -/** - * Parse a derivation path and return an array of indexes. - * @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki - * @returns {Number[]} - */ - -HDPrivateKey._getIndexes = function _getIndexes(path) { - var steps = path.split('/'); - var root = steps.shift(); - var indexes = []; - var i, step, hardened, index; - - if (~constants.hd.PATH_ROOTS.indexOf(path)) - return indexes; - - if (!~constants.hd.PATH_ROOTS.indexOf(root)) - return null; - - for (i = 0; i < steps.length; i++) { - step = steps[i]; - hardened = step[step.length - 1] === '\''; - - if (hardened) - step = step.slice(0, -1); - - if (!step || step[0] === '-') - return null; - - index = +step; - - if (hardened) - index += constants.hd.HARDENED; - - indexes.push(index); - } - - return indexes; -}; - /** * Test whether a string is a valid path. * @param {String} path @@ -640,21 +648,13 @@ HDPrivateKey._getIndexes = function _getIndexes(path) { * @returns {Boolean} */ -HDPrivateKey.isValidPath = function isValidPath(path, hardened) { - var indexes; - - if (typeof path === 'string') { - indexes = HDPrivateKey._getIndexes(path); - return indexes !== null && indexes.every(HDPrivateKey.isValidPath); +HDPrivateKey.isValidPath = function isValidPath(path) { + try { + HD.parsePath(path, constants.hd.MAX_INDEX); + return true; + } catch (e) { + return false; } - - if (typeof path === 'number') { - if (path < constants.hd.HARDENED && hardened) - path += constants.hd.HARDENED; - return path >= 0 && path < constants.hd.MAX_INDEX; - } - - return false; }; /** @@ -665,16 +665,14 @@ HDPrivateKey.isValidPath = function isValidPath(path, hardened) { */ HDPrivateKey.prototype.derivePath = function derivePath(path) { - var indexes; + var indexes = HD.parsePath(path, constants.hd.MAX_INDEX); + var key = this; + var i; - if (!HDPrivateKey.isValidPath(path)) - throw new Error('Invalid path.'); + for (i = 0; i < indexes.length; i++) + key = key.derive(indexes[i]); - indexes = HDPrivateKey._getIndexes(path); - - return indexes.reduce(function(prev, index) { - return prev.derive(index); - }, this); + return key; }; /** @@ -970,7 +968,10 @@ HDPrivateKey.fromJSON = function fromJSON(json, passphrase) { */ HDPrivateKey.isHDPrivateKey = function isHDPrivateKey(obj) { - return obj && obj.xprivkey && typeof obj.derive === 'function'; + return obj + && obj.hdPublicKey + && obj.hdPublicKey !== obj + && typeof obj.derive === 'function'; }; /** @@ -1127,18 +1128,13 @@ HDPublicKey.prototype.isAccount44 = HDPrivateKey.prototype.isAccount44; * @returns {Boolean} */ -HDPublicKey.isValidPath = function isValidPath(arg) { - var indexes; - - if (typeof arg === 'string') { - indexes = HDPrivateKey._getIndexes(arg); - return indexes !== null && indexes.every(HDPublicKey.isValidPath); +HDPublicKey.isValidPath = function isValidPath(path) { + try { + HD.parsePath(path, constants.hd.HARDENED); + return true; + } catch (e) { + return false; } - - if (typeof arg === 'number') - return arg >= 0 && arg < constants.hd.HARDENED; - - return false; }; /** @@ -1150,19 +1146,14 @@ HDPublicKey.isValidPath = function isValidPath(arg) { */ HDPublicKey.prototype.derivePath = function derivePath(path) { - var indexes; + var indexes = HD.parsePath(path, constants.hd.HARDENED); + var key = this; + var i; - if (path.indexOf('\'') !== -1) - throw new Error('Cannot derive hardened.'); + for (i = 0; i < indexes.length; i++) + key = key.derive(indexes[i]); - if (!HDPublicKey.isValidPath(path)) - throw new Error('Invalid path.'); - - indexes = HDPrivateKey._getIndexes(path); - - return indexes.reduce(function(prev, index) { - return prev.derive(index); - }, this); + return key; }; /** @@ -1291,8 +1282,8 @@ HDPublicKey.fromBase58 = function fromBase58(xkey) { HDPublicKey.isHDPublicKey = function isHDPublicKey(obj) { return obj - && obj.xpubkey - && !obj.xprivkey + && obj.hdPublicKey + && obj.hdPublicKey === obj && typeof obj.derive === 'function'; }; @@ -1352,10 +1343,28 @@ HDPublicKey.isHDPublicKey = function isHDPublicKey(obj) { * @returns {Base58String} */ -HDPrivateKey.prototype.toSecret = function toSecret() { - return KeyPair.prototype.toSecret.call(this); +HDPrivateKey.prototype.toSecret = function toSecret(network) { + return KeyPair.prototype.toSecret.call(this, network); }; +/* + * Helpers + */ + +function nfkd(str) { + if (str.normalize) + return str.normalize('NFKD'); + + if (!unorm) + unorm = require('../../vendor/unorm'); + + return unorm.nfkd(str); +} + +/* + * Expose + */ + HD.Mnemonic = Mnemonic; HD.PrivateKey = HDPrivateKey; HD.PublicKey = HDPublicKey; diff --git a/lib/bcoin/keypair.js b/lib/bcoin/keypair.js index 3f0be566..e50dbfc6 100644 --- a/lib/bcoin/keypair.js +++ b/lib/bcoin/keypair.js @@ -128,8 +128,11 @@ KeyPair.prototype.getPublicKey = function getPublicKey(enc) { * @returns {Base58String} */ -KeyPair.prototype.toSecret = function toSecret() { - return KeyPair.toSecret(this.getPrivateKey(), this.compressed, this.network); +KeyPair.prototype.toSecret = function toSecret(network) { + if (!network) + network = this.network; + + return KeyPair.toSecret(this.getPrivateKey(), this.compressed, network); }; /** @@ -180,7 +183,7 @@ KeyPair.parseSecret = function parseSecret(secret) { privateKey = p.readBytes(32); if (p.left() > 4) { - assert(p.readU8() === 1); + assert(p.readU8() === 1, 'Bad compression flag.'); compressed = true; }