diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index ed7d2d0a..753c7390 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -58,6 +58,7 @@ var bn = require('bn.js'); var elliptic = require('elliptic'); var utils = bcoin.utils; var assert = utils.assert; +var constants = bcoin.protocol.constants; var network = bcoin.protocol.network; var EventEmitter = require('events').EventEmitter; @@ -74,12 +75,12 @@ function HDSeed(options) { if (!(this instanceof HDSeed)) return new HDSeed(options); + options = options || {}; + this.bits = options.bits || 128; this.entropy = options.entropy || HDSeed._entropy(this.bits / 8); this.mnemonic = options.mnemonic || HDSeed._mnemonic(this.entropy); - if (options.passphrase !== undefined) { - this.seed = this.createSeed(options.passphrase); - } + this.seed = this.createSeed(options.passphrase); } HDSeed.create = function(options) { @@ -112,40 +113,37 @@ HDSeed._mnemonic = function(entropy) { return mnemonic.join(' '); }; -/** - * HD Keys - */ - -var HARDENED = 0x80000000; -var MAX_INDEX = 2 * HARDENED; -var MIN_ENTROPY = 128 / 8; -var MAX_ENTROPY = 512 / 8; -var PARENT_FINGER_PRINT_SIZE = 4; -var PATH_ROOTS = ['m', 'M', 'm\'', 'M\'']; - /** * HD Private Key */ -function HDPriv(options) { +function HDPrivateKey(options) { var data; - if (!(this instanceof HDPriv)) - return new HDPriv(options); + if (!(this instanceof HDPrivateKey)) + return new HDPrivateKey(options); if (!options) - options = { seed: new HDSeed({ passphrase: '' }) }; + options = { seed: bcoin.hd.seed() }; if (typeof options === 'string' && options.indexOf('xprv') === 0) options = { xkey: options }; - if (options.passphrase !== undefined) - options.seed = new HDSeed({ passphrase: options.passphrase }); + if (options.passphrase !== undefined + || options.bits + || options.entropy + || options.mnemonic) { + options.seed = bcoin.hd.seed(options); + } + + if (options.seed + && typeof options.seed === 'object' + && !Array.isArray(options.seed) + && !(options.seed instanceof bcoin.hd.seed)) { + options.seed = bcoin.hd.seed(options.seed); + } if (options.seed) { - if (typeof options.seed === 'object' && !(options.seed instanceof HDSeed)) { - options.seed = new HDSeed(options.seed); - } this.seed = options.seed; data = this._seed(options.seed); } else if (options.xkey) { @@ -163,26 +161,44 @@ function HDPriv(options) { this._build(data); } -HDPriv.prototype._normalize = function(data, version) { - var b; - +HDPrivateKey.prototype._normalize = function(data, version) { data.version = version || network.prefixes.xprivkey; - data.version = +data.version; + data.privateKey = data.privateKey || data.priv; + data.publicKey = data.publicKey || data.pub; - data.depth = +data.depth; - - if (typeof data.parentFingerPrint === 'number') { - b = []; - utils.writeU32BE(b, data.parentFingerPrint, 0); - data.parentFingerPrint = b; + // version = uint_32be + if (typeof data.version === 'string') { + data.version = utils.toArray(data.version, 'hex'); + } else if (typeof data.version === 'number') { + data.version = array32(data.version); } - data.childIndex = +data.childIndex; + // depth = unsigned char + if (typeof data.depth === 'string') + data.depth = utils.toArray(data.depth, 'hex'); + else if (typeof data.depth === 'number') + data.depth = [data.depth]; + if (new bn(data.depth).toNumber() > 0xff) + throw new Error('Depth is too high'); + + // parent finger print = uint_32be + if (typeof data.parentFingerPrint === 'string') + data.parentFingerPrint = utils.toArray(data.parentFingerPrint, 'hex'); + else if (typeof data.parentFingerPrint === 'number') + data.parentFingerPrint = array32(data.parentFingerPrint); + + // child index = uint_32be + if (typeof data.childIndex === 'string') + data.childIndex = utils.toArray(data.childIndex, 'hex'); + else if (typeof data.childIndex === 'number') + data.childIndex = array32(data.childIndex); + + // chain code = 32 bytes if (typeof data.chainCode === 'string') data.chainCode = utils.toArray(data.chainCode, 'hex'); - data.privateKey = data.privateKey || data.priv; + // private key = 32 bytes if (data.privateKey) { if (data.privateKey.getPrivate) data.privateKey = data.privateKey.getPrivate().toArray(); @@ -190,7 +206,7 @@ HDPriv.prototype._normalize = function(data, version) { data.privateKey = utils.toKeyArray(data.privateKey); } - data.publicKey = data.publicKey || data.pub; + // public key = 33 bytes if (data.publicKey) { if (data.publicKey.getPublic) data.publicKey = data.privateKey.getPublic(true, 'array'); @@ -198,29 +214,31 @@ HDPriv.prototype._normalize = function(data, version) { data.publicKey = utils.toKeyArray(data.publicKey); } - if (typeof data.checksum === 'number') { - b = []; - utils.writeU32BE(b, data.checksum, 0); - data.checksum = b; - } + // checksum = 4 bytes + if (typeof data.checksum === 'string') + data.checksum = utils.toArray(data.checksum, 'hex'); + else if (typeof data.checksum === 'number') + data.checksum = array32(data.checksum); return data; }; -HDPriv.prototype._seed = function(seed) { - if (seed instanceof HDSeed) +HDPrivateKey.prototype._seed = function(seed) { + if (seed instanceof bcoin.hd.seed) seed = seed.seed; if (utils.isHex(seed)) seed = utils.toArray(seed, 'hex'); - if (seed.length < MIN_ENTROPY || seed.length > MAX_ENTROPY) + if (seed.length < constants.hd.minEntropy + || seed.length > constants.hd.maxEntropy) { throw new Error('entropy not in range'); + } var hash = sha512hmac(seed, 'Bitcoin seed'); return { - // version: network.prefixes.xprivkey, + version: null, depth: 0, parentFingerPrint: 0, childIndex: 0, @@ -230,7 +248,7 @@ HDPriv.prototype._seed = function(seed) { }; }; -HDPriv.prototype._unbuild = function(xkey) { +HDPrivateKey.prototype._unbuild = function(xkey) { var raw = utils.fromBase58(xkey); var data = {}; var off = 0; @@ -259,28 +277,28 @@ HDPriv.prototype._unbuild = function(xkey) { return data; }; -HDPriv.prototype._build = function(data) { +HDPrivateKey.prototype._build = function(data) { var sequence = []; var off = 0; var checksum, xprivkey, pair, privateKey, publicKey, size, fingerPrint; - utils.writeU32BE(sequence, data.version, off); - off += 4; - sequence[off] = data.depth; - off += 1; + utils.copy(data.version, sequence, off, true); + off += data.version.length; + utils.copy(data.depth, sequence, off, true); + off += data.depth.length; utils.copy(data.parentFingerPrint, sequence, off, true); off += data.parentFingerPrint.length; - utils.writeU32BE(sequence, data.childIndex, off); - off += 4; + utils.copy(data.childIndex, sequence, off, true); + off += data.childIndex.length; utils.copy(data.chainCode, sequence, off, true); off += data.chainCode.length; - sequence[off] = 0; - off += 1; + utils.copy([0], sequence, off, true); + off += [0].length; utils.copy(data.privateKey, sequence, off, true); off += data.privateKey.length; checksum = utils.dsha256(sequence).slice(0, 4); utils.copy(checksum, sequence, off, true); - off += 4; + off += checksum.length; xprivkey = utils.toBase58(sequence); @@ -288,7 +306,7 @@ HDPriv.prototype._build = function(data) { privateKey = pair.getPrivate().toArray(); publicKey = pair.getPublic(true, 'array'); - size = PARENT_FINGER_PRINT_SIZE; + size = constants.hd.parentFingerPrintSize; fingerPrint = utils.ripesha(publicKey).slice(0, size); this.version = data.version; @@ -297,33 +315,30 @@ HDPriv.prototype._build = function(data) { this.childIndex = data.childIndex; this.chainCode = data.chainCode; this.privateKey = privateKey; - // this.checksum = checksum; + this.checksum = null; this.xprivkey = xprivkey; this.fingerPrint = fingerPrint; this.publicKey = publicKey; - this.hdpub = new HDPub(this); + this.hdpub = bcoin.hd.pub(this); this.xpubkey = this.hdpub.xpubkey; this.pair = bcoin.ecdsa.keyPair({ priv: this.privateKey }); }; -HDPriv.prototype.derive = function(index, hard) { +HDPrivateKey.prototype.derive = function(index, hardened) { + var data, hash, leftPart, chainCode, privateKey; + if (typeof index === 'string') return this.deriveString(index); - var index_ = []; - var data, hash, leftPart, chainCode, privateKey; + hardened = index >= constants.hd.hardened ? true : hardened; + if (index < constants.hd.hardened && hardened) + index += constants.hd.hardened; - hard = index >= HARDENED ? true : hard; - if (index < HARDENED && hard === true) - index += HARDENED; - - utils.writeU32BE(index_, index, 0); - - data = hard - ? [0].concat(this.privateKey).concat(index_) - : data = [].concat(this.publicKey).concat(index_); + data = hardened + ? [0].concat(this.privateKey).concat(array32(index)) + : data = [].concat(this.publicKey).concat(array32(index)); hash = sha512hmac(data, this.chainCode); leftPart = new bn(hash.slice(0, 32)); @@ -331,43 +346,44 @@ HDPriv.prototype.derive = function(index, hard) { privateKey = leftPart.add(new bn(this.privateKey)).mod(ec.curve.n).toArray(); - return new HDPriv({ + return new HDPrivateKey({ + version: null, master: this.master, - // version: this.version, - depth: this.depth + 1, + depth: new bn(this.depth).toNumber() + 1, parentFingerPrint: this.fingerPrint, childIndex: index, chainCode: chainCode, - privateKey: privateKey + privateKey: privateKey, + checksum: null }); }; // https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki -HDPriv._getIndexes = function(path) { +HDPrivateKey._getIndexes = function(path) { var steps = path.split('/'); var root = steps.shift(); var indexes = []; - var i, step, hard, index; + var i, step, hardened, index; - if (~PATH_ROOTS.indexOf(path)) + if (~constants.hd.pathRoots.indexOf(path)) return indexes; - if (!~PATH_ROOTS.indexOf(root)) + if (!~constants.hd.pathRoots.indexOf(root)) return null; for (i = 0; i < steps.length; i++) { step = steps[i]; - hard = step[step.length - 1] === '\''; + hardened = step[step.length - 1] === '\''; - if (hard) + if (hardened) step = step.slice(0, -1); if (!step || step[0] === '-') return null; index = +step; - if (hard) - index += HARDENED; + if (hardened) + index += constants.hd.hardened; indexes.push(index); } @@ -375,42 +391,45 @@ HDPriv._getIndexes = function(path) { return indexes; }; -HDPriv.isValidPath = function(path, hard) { +HDPrivateKey.isValidPath = function(path, hardened) { if (typeof path === 'string') { - var indexes = HDPriv._getIndexes(path); - return indexes !== null && indexes.every(HDPriv.isValidPath); + var indexes = HDPrivateKey._getIndexes(path); + return indexes !== null && indexes.every(HDPrivateKey.isValidPath); } if (typeof path === 'number') { - if (path < HARDENED && hard === true) { - path += HARDENED; + if (path < constants.hd.hardened && hardened) { + path += constants.hd.hardened; } - return path >= 0 && path < MAX_INDEX; + return path >= 0 && path < constants.hd.maxIndex; } return false; }; -HDPriv.prototype.deriveString = function(path) { - if (!HDPriv.isValidPath(path)) +HDPrivateKey.prototype.deriveString = function(path) { + if (!HDPrivateKey.isValidPath(path)) throw new Error('invalid path'); - var indexes = HDPriv._getIndexes(path); + var indexes = HDPrivateKey._getIndexes(path); return indexes.reduce(function(prev, index) { return prev.derive(index); - }); + }, this); }; /** * HD Public Key */ -function HDPub(options) { +function HDPublicKey(options) { var data; - if (!(this instanceof HDPub)) - return new HDPub(options); + if (!(this instanceof HDPublicKey)) + return new HDPublicKey(options); + + if (!options) + throw new Error('No options for HDPublicKey'); if (typeof options === 'string' && options.indexOf('xpub') === 0) options = { xkey: options }; @@ -426,9 +445,9 @@ function HDPub(options) { this._build(data); } -HDPub.prototype._normalize = HDPriv.prototype._normalize; +HDPublicKey.prototype._normalize = HDPrivateKey.prototype._normalize; -HDPub.prototype._unbuild = function(xkey) { +HDPublicKey.prototype._unbuild = function(xkey) { var raw = utils.fromBase58(xkey); var data = {}; var off = 0; @@ -456,26 +475,26 @@ HDPub.prototype._unbuild = function(xkey) { return data; }; -HDPub.prototype._build = function(data) { +HDPublicKey.prototype._build = function(data) { var sequence = []; var off = 0; var checksum, xpubkey, publicKey, size, fingerPrint; - utils.writeU32BE(sequence, data.version, off); - off += 4; - sequence[off] = data.depth; - off += 1; + utils.copy(data.version, sequence, off, true); + off += data.version.length; + utils.copy(data.depth, sequence, off, true); + off += data.depth.length; utils.copy(data.parentFingerPrint, sequence, off, true); off += data.parentFingerPrint.length; - utils.writeU32BE(sequence, data.childIndex, off); - off += 4; + utils.copy(data.childIndex, sequence, off, true); + off += data.childIndex.length; utils.copy(data.chainCode, sequence, off, true); off += data.chainCode.length; utils.copy(data.publicKey, sequence, off, true); off += data.publicKey.length; checksum = utils.dsha256(sequence).slice(0, 4); utils.copy(checksum, sequence, off, true); - off += 4; + off += checksum.length; if (!data.checksum || !data.checksum.length) data.checksum = checksum; @@ -485,7 +504,7 @@ HDPub.prototype._build = function(data) { xpubkey = utils.toBase58(sequence); publicKey = data.publicKey; - size = PARENT_FINGER_PRINT_SIZE; + size = constants.hd.parentFingerPrintSize; fingerPrint = utils.ripesha(publicKey).slice(0, size); this.version = data.version; @@ -494,7 +513,7 @@ HDPub.prototype._build = function(data) { this.childIndex = data.childIndex; this.chainCode = data.chainCode; this.publicKey = publicKey; - // this.checksum = checksum; + this.checksum = null; this.xpubkey = xpubkey; this.fingerPrint = fingerPrint; @@ -503,22 +522,19 @@ HDPub.prototype._build = function(data) { this.pair = bcoin.ecdsa.keyPair({ pub: this.publicKey }); }; -HDPub.prototype.derive = function(index, hard) { - var index_ = []; +HDPublicKey.prototype.derive = function(index, hardened) { var data, hash, leftPart, chainCode, pair, pubkeyPoint, publicKey; if (typeof index === 'string') return this.deriveString(index); - if (index >= HARDENED || hard) + if (index >= constants.hd.hardened || hardened) throw new Error('invalid index'); if (index < 0) throw new Error('invalid path'); - utils.writeU32BE(index_, index, 0); - - data = [].concat(this.publicKey).concat(index_); + data = [].concat(this.publicKey).concat(array32(index)); hash = sha512hmac(data, this.chainCode); leftPart = new bn(hash.slice(0, 32)); chainCode = hash.slice(32, 64); @@ -527,46 +543,47 @@ HDPub.prototype.derive = function(index, hard) { pubkeyPoint = ec.curve.g.mul(leftPart).add(pair.pub); publicKey = bcoin.ecdsa.keyFromPublic(pubkeyPoint).getPublic(true, 'array'); - return new HDPub({ - // version: network.prefixes.xpubkey, - depth: this.depth + 1, + return new HDPublicKey({ + version: null, + depth: new bn(this.depth).toNumber() + 1, parentFingerPrint: this.fingerPrint, childIndex: index, chainCode: chainCode, - publicKey: publicKey + publicKey: publicKey, + checksum: null }); }; -HDPub.isValidPath = function(arg) { +HDPublicKey.isValidPath = function(arg) { if (typeof arg === 'string') { - var indexes = HDPriv._getIndexes(arg); - return indexes !== null && indexes.every(HDPub.isValidPath); + var indexes = HDPrivateKey._getIndexes(arg); + return indexes !== null && indexes.every(HDPublicKey.isValidPath); } if (typeof arg === 'number') - return arg >= 0 && arg < HARDENED; + return arg >= 0 && arg < constants.hd.hardened; return false; }; -HDPub.prototype.deriveString = function(path) { +HDPublicKey.prototype.deriveString = function(path) { if (~path.indexOf('\'')) throw new Error('cannot derive hardened'); - else if (!HDPub.isValidPath(path)) + else if (!HDPublicKey.isValidPath(path)) throw new Error('invalid path'); - var indexes = HDPriv._getIndexes(path); + var indexes = HDPrivateKey._getIndexes(path); return indexes.reduce(function(prev, index) { return prev.derive(index); - }); + }, this); }; /** * Make HD keys behave like elliptic KeyPairs */ -[HDPriv, HDPub].forEach(function(HD) { +[HDPrivateKey, HDPublicKey].forEach(function(HD) { HD.prototype.validate = function validate() { return this.pair.validate.apply(this.pair, arguments); }; @@ -689,11 +706,17 @@ function pbkdf2(key, salt, iterations, dkLen) { return DK; } +function array32(data) { + var b = []; + utils.writeU32BE(b, data, 0); + return b; +} + /** * Expose */ hd.seed = HDSeed; -hd.priv = HDPriv; -hd.pub = HDPub; +hd.priv = HDPrivateKey; +hd.pub = HDPublicKey; hd.pbkdf2 = pbkdf2; diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 06ca07e7..4829fcf9 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -192,3 +192,12 @@ exports.zeroHash = utils.toArray( '0000000000000000000000000000000000000000000000000000000000000000', 'hex' ); + +exports.hd = { + hardened: 0x80000000, + maxIndex: 2 * 0x80000000, + minEntropy: 128 / 8, + maxEntropy: 512 / 8, + parentFingerPrintSize: 4, + pathRoots: ['m', 'M', 'm\'', 'M\''] +};