From c9bdad9bdfa01a3ccf43f605572fc0ac72a6a448 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 1 Apr 2016 21:09:06 -0700 Subject: [PATCH] hd rewrite. --- lib/bcoin/hd.js | 806 +++++++++++++++++++++----------------------- lib/bcoin/reader.js | 15 + lib/bcoin/wallet.js | 10 +- lib/bcoin/writer.js | 6 + test/hd-test.js | 8 +- 5 files changed, 406 insertions(+), 439 deletions(-) diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index 6fcad203..bc642fde 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -59,6 +59,8 @@ var constants = bcoin.protocol.constants; var network = bcoin.protocol.network; var KeyPair = require('./keypair'); var LRU = require('./lru'); +var BufferWriter = require('./writer'); +var BufferReader = require('./reader'); var english = require('../../etc/english.json'); @@ -91,7 +93,10 @@ HDSeed.prototype.createSeed = function createSeed() { if (!this.mnemonic) this.mnemonic = this.createMnemonic(this.entropy); - this.seed = utils.pbkdf2(this.mnemonic, 'mnemonic' + this.passphrase, 2048, 64); + this.seed = utils.pbkdf2( + this.mnemonic, + 'mnemonic' + this.passphrase, + 2048, 64); return this.seed; }; @@ -126,22 +131,29 @@ function HD(options) { return new HDPrivateKey(options); } -HD.generate = function generate(privateKey, entropy) { - return HDPrivateKey.generate(privateKey, entropy); +HD.fromBase58 = function fromBase58(xkey) { + if (HDPrivateKey.isExtended(xkey)) + return HDPrivateKey.fromBase58(xkey); + return HDPublicKey.fromBase58(xkey); }; -HD.fromSeed = function fromSeed(options) { - return HDPrivateKey.fromSeed(options); +HD.generate = function generate(options, networkType) { + return HDPrivateKey.generate(options, networkType); +}; + +HD.fromSeed = function fromSeed(options, networkType) { + return HDPrivateKey.fromSeed(options, networkType); }; HD.cache = new LRU(500); HD.isHD = function isHD(obj) { - return HDPrivateKey.isHDPrivateKey(obj) || HDPublicKey.isHDPublicKey(obj); + return HDPrivateKey.isHDPrivateKey(obj) + || HDPublicKey.isHDPublicKey(obj); }; /** - * HD Private Key + * HDPrivateKey */ function HDPrivateKey(options) { @@ -150,11 +162,10 @@ function HDPrivateKey(options) { if (!(this instanceof HDPrivateKey)) return new HDPrivateKey(options); + assert(options, 'No options for HD private key.'); assert(!(options instanceof HDPrivateKey)); assert(!(options instanceof HDPublicKey)); - assert(options); - if (HDPrivateKey.isExtended(options)) options = { xkey: options }; @@ -167,39 +178,125 @@ function HDPrivateKey(options) { if (HDPublicKey.isExtended(options.xkey)) return new HDPublicKey(options); - this.network = options.network || network.type; + this.networkType = options.networkType || network.type; + this.xprivkey = options.xkey; this.seed = options.seed; - if (options.xkey) { - data = this._unbuild(options.xkey); - } else if (options.seed) { - data = this._seed(options.seed); - } else { + if (options.data) { data = options.data; + } else if (options.xkey) { + data = HDPrivateKey.parse(options.xkey); + this.networkType = data.networkType; + data = data.data; + } else if (options.seed) { + data = HDPrivateKey._fromSeed(options.seed, this.networkType); + } else { + assert(false, 'No data passed to HD key.'); } - assert(data); - data = this._normalize(data); + assert(data.depth <= 0xff, 'Depth is too high.'); - this.data = data; + this.version = data.version; + this.depth = data.depth; + this.parentFingerPrint = data.parentFingerPrint; + this.childIndex = data.childIndex; + this.chainCode = data.chainCode; + this.privateKey = data.privateKey; - this._build(data); + this.publicKey = ec.publicKeyCreate(data.privateKey, true); + this.fingerPrint = null; - if (utils.readU32BE(data.parentFingerPrint) === 0) - this.isMaster = true; - else - this.isMaster = false; + this.hdPrivateKey = this; + + if (!this.xprivkey) + this.xprivkey = HDPrivateKey.render(data); this.isPrivate = true; this.isPublic = false; } -HDPublicKey.isHDPrivateKey = function isHDPrivateKey(obj) { - return obj && obj.isPrivate && typeof obj._unbuild === 'function'; -}; - utils.inherits(HDPrivateKey, HD); +HDPrivateKey.prototype.__defineGetter__('hdPublicKey', function() { + if (!this._hdPublicKey) { + this._hdPublicKey = new HDPublicKey({ + networkType: this.networkType, + data: { + version: network[this.networkType].prefixes.xpubkey, + depth: this.depth, + parentFingerPrint: this.parentFingerPrint, + childIndex: this.childIndex, + chainCode: this.chainCode, + publicKey: this.publicKey + } + }); + } + return this._hdPublicKey; +}); + +HDPrivateKey.prototype.__defineGetter__('xpubkey', function() { + return this.hdPublicKey.xpubkey; +}); + +HDPrivateKey.prototype.derive = function derive(index, hardened) { + var cached, p, data, hash, leftPart, chainCode, privateKey, child; + + if (typeof index === 'string') + return this.derivePath(index); + + cached = HD.cache.get(this.xprivkey + '/' + index); + + if (cached) + return cached; + + hardened = index >= constants.hd.hardened ? true : hardened; + if (index < constants.hd.hardened && hardened) + index += constants.hd.hardened; + + p = new BufferWriter(); + + if (hardened) { + p.writeU8(0); + p.writeBytes(this.privateKey); + p.writeU32BE(index); + } else { + p.writeBytes(this.publicKey); + p.writeU32BE(index); + } + + data = p.render(); + + hash = utils.sha512hmac(data, this.chainCode); + leftPart = new bn(hash.slice(0, 32)); + chainCode = hash.slice(32, 64); + + privateKey = leftPart + .add(new bn(this.privateKey)) + .mod(ec.elliptic.curve.n) + .toBuffer('be', 32); + + if (!this.fingerPrint) { + this.fingerPrint = utils.ripesha(this.publicKey) + .slice(0, constants.hd.parentFingerPrintSize); + } + + child = new HDPrivateKey({ + networkType: this.networkType, + data: { + version: this.version, + depth: this.depth + 1, + parentFingerPrint: this.fingerPrint, + childIndex: index, + chainCode: chainCode, + privateKey: privateKey + } + }); + + HD.cache.set(this.xprivkey + '/' + index, child); + + return child; +}; + HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(options) { var coinType, accountIndex, child; @@ -215,7 +312,7 @@ HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(options) { } if (coinType == null) - coinType = network[this.network].type === 'main' ? 0 : 1; + coinType = this.networkType === 'main' ? 0 : 1; assert(utils.isFinite(coinType)); assert(utils.isFinite(accountIndex)); @@ -246,15 +343,15 @@ HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45() { }; HDPrivateKey.prototype.isPurpose45 = function isPurpose45() { - if (utils.readU8(this.depth) !== 1) + if (this.depth !== 1) return false; - return utils.readU32BE(this.childIndex) === constants.hd.hardened + 45; + return this.childIndex === constants.hd.hardened + 45; }; HDPrivateKey.prototype.isAccount44 = function isAccount44() { - if (utils.readU32BE(this.childIndex) < constants.hd.hardened) + if (this.childIndex < constants.hd.hardened) return false; - return utils.readU8(this.depth) === 3; + return this.depth === 3; }; HDPrivateKey.isExtended = function isExtended(data) { @@ -264,258 +361,6 @@ HDPrivateKey.isExtended = function isExtended(data) { return network.xprivkeys[data.slice(0, 4)]; }; -HDPrivateKey.prototype._normalize = function _normalize(data) { - if (!data.version) { - data.version = (this instanceof HDPrivateKey) - ? network[this.network].prefixes.xprivkey - : network[this.network].prefixes.xpubkey; - } - - // version = uint_32be - if (typeof data.version === 'number') - data.version = array32(data.version); - - // depth = unsigned char - if (typeof data.depth === 'number') - data.depth = new Buffer([data.depth]); - - if (data.depth.length > 1) - throw new Error('Depth is too high'); - - // parent finger print = uint_32be - if (typeof data.parentFingerPrint === 'number') - data.parentFingerPrint = array32(data.parentFingerPrint); - - // child index = uint_32be - if (typeof data.childIndex === 'number') - data.childIndex = array32(data.childIndex); - - // chain code = 32 bytes - if (typeof data.chainCode === 'string') - data.chainCode = new Buffer(data.chainCode, 'hex'); - - // checksum = 4 bytes - if (typeof data.checksum === 'string') - data.checksum = new Buffer(data.checksum, 'hex'); - - return data; -}; - -HDPrivateKey.prototype._seed = function _seed(seed) { - var hash; - - if (seed instanceof HDSeed) - seed = seed.createSeed(); - - if (utils.isHex(seed)) - seed = new Buffer(seed, 'hex'); - - if (seed.length < constants.hd.minEntropy - || seed.length > constants.hd.maxEntropy) { - throw new Error('entropy not in range'); - } - - hash = utils.sha512hmac(seed, 'Bitcoin seed'); - - return { - version: array32(network[this.network].prefixes.xprivkey), - depth: new Buffer([0]), - parentFingerPrint: new Buffer([0, 0, 0, 0]), - childIndex: new Buffer([0, 0, 0, 0]), - chainCode: hash.slice(32, 64), - privateKey: hash.slice(0, 32), - checksum: null - }; -}; - -HDPrivateKey.fromSeed = function fromSeed(options) { - var seed = (options instanceof HDSeed) ? options : new HDSeed(options); - return new HDPrivateKey({ seed: seed }); -}; - -HDPrivateKey._generate = function _generate(privateKey, entropy) { - if (!privateKey) - privateKey = ec.generatePrivateKey(); - - if (!entropy) - entropy = ec.random(32); - - return { - version: array32(network.prefixes.xprivkey), - depth: new Buffer([0]), - parentFingerPrint: new Buffer([0, 0, 0, 0]), - childIndex: new Buffer([0, 0, 0, 0]), - chainCode: entropy, - privateKey: privateKey, - checksum: null - }; -}; - -HDPrivateKey.generate = function generate(privateKey, entropy) { - return new HDPrivateKey(HDPrivateKey._generate(privateKey, entropy)); -}; - -HDPrivateKey.prototype._generate = function _generate(privateKey, entropy) { - var data = HDPrivateKey._generate(privateKey, entropy); - data.version = array32(network[this.network].prefixes.xprivkey); - return data; -}; - -HDPrivateKey.prototype._unbuild = function _unbuild(xkey) { - var raw = utils.fromBase58(xkey); - var data = {}; - var off = 0; - var hash; - - data.version = raw.slice(off, off + 4); - off += 4; - data.depth = raw.slice(off, off + 1); - off += 1; - data.parentFingerPrint = raw.slice(off, off + 4); - off += 4; - data.childIndex = raw.slice(off, off + 4); - off += 4; - data.chainCode = raw.slice(off, off + 32); - off += 32; - off += 1; // nul byte - data.privateKey = raw.slice(off, off + 32); - off += 32; - data.checksum = raw.slice(off, off + 4); - off += 4; - - hash = utils.dsha256(raw.slice(0, -4)).slice(0, 4); - if (!utils.isEqual(data.checksum, hash)) - throw new Error('checksum mismatch'); - - network.types.some(function(type) { - if (utils.readU32BE(data.version) === network[type].prefixes.xprivkey) { - this.network = type; - return true; - } - return false; - }, this); - - this.xprivkey = xkey; - - return data; -}; - -HDPrivateKey.prototype._build = function _build(data) { - var off = 0; - var sequence, checksum; - - if (!this.xprivkey) { - sequence = new Buffer(82); - off += utils.copy(data.version, sequence, off); - off += utils.copy(data.depth, sequence, off); - off += utils.copy(data.parentFingerPrint, sequence, off); - off += utils.copy(data.childIndex, sequence, off); - off += utils.copy(data.chainCode, sequence, off); - off += utils.writeU8(sequence, 0, off); - off += utils.copy(data.privateKey, sequence, off); - assert(off === 78, off); - checksum = utils.dsha256(sequence.slice(0, off)).slice(0, 4); - off += utils.copy(checksum, sequence, off); - assert(off === 82, off); - - this.xprivkey = utils.toBase58(sequence); - } - - this.version = data.version; - this.depth = data.depth; - this.parentFingerPrint = data.parentFingerPrint; - this.childIndex = data.childIndex; - this.chainCode = data.chainCode; - this.privateKey = data.privateKey; - this.checksum = null; - - this.publicKey = ec.publicKeyCreate(data.privateKey, true); - this.fingerPrint = null; - - this.hdPrivateKey = this; -}; - -HDPrivateKey.prototype.__defineGetter__('hdPublicKey', function() { - if (!this._hdPublicKey) { - this._hdPublicKey = new HDPublicKey({ - network: this.network, - data: { - version: array32(network[this.network].prefixes.xpubkey), - depth: this.depth, - parentFingerPrint: this.parentFingerPrint, - childIndex: this.childIndex, - chainCode: this.chainCode, - checksum: this.checksum, - publicKey: this.publicKey - } - }); - } - return this._hdPublicKey; -}); - -HDPrivateKey.prototype.__defineGetter__('xpubkey', function() { - return this.hdPublicKey.xpubkey; -}); - -HDPrivateKey.prototype.derive = function derive(index, hardened) { - var cached, data, hash, leftPart, chainCode, privateKey, child; - var off = 0; - - if (typeof index === 'string') - return this.derivePath(index); - - cached = HD.cache.get(this.xprivkey + '/' + index); - - if (cached) - return cached; - - hardened = index >= constants.hd.hardened ? true : hardened; - if (index < constants.hd.hardened && hardened) - index += constants.hd.hardened; - - if (hardened) { - data = new Buffer(1 + this.privateKey.length + 4); - off += utils.writeU8(data, 0, off); - off += utils.copy(this.privateKey, data, off); - off += utils.writeU32BE(data, index, off); - } else { - data = new Buffer(this.publicKey.length + 4); - off += utils.copy(this.publicKey, data, off); - off += utils.writeU32BE(data, index, off); - } - - hash = utils.sha512hmac(data, this.chainCode); - leftPart = new bn(hash.slice(0, 32)); - chainCode = hash.slice(32, 64); - - privateKey = leftPart - .add(new bn(this.privateKey)) - .mod(ec.elliptic.curve.n) - .toBuffer('be', 32); - - if (!this.fingerPrint) { - this.fingerPrint = utils.ripesha(this.publicKey) - .slice(0, constants.hd.parentFingerPrintSize); - } - - child = new HDPrivateKey({ - network: this.network, - data: { - version: this.version, - depth: utils.readU8(this.depth) + 1, - parentFingerPrint: this.fingerPrint, - childIndex: index, - chainCode: chainCode, - privateKey: privateKey, - checksum: null - } - }); - - HD.cache.set(this.xprivkey + '/' + index, child); - - return child; -}; - // https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki HDPrivateKey._getIndexes = function _getIndexes(path) { var steps = path.split('/'); @@ -580,6 +425,128 @@ HDPrivateKey.prototype.derivePath = function derivePath(path) { }, this); }; +HDPrivateKey._fromSeed = function _fromSeed(seed, networkType) { + var data = seed.createSeed(); + var hash; + + if (data.length < constants.hd.minEntropy + || data.length > constants.hd.maxEntropy) { + throw new Error('Entropy not in range.'); + } + + hash = utils.sha512hmac(data, 'Bitcoin seed'); + + return { + version: networkType + ? network[networkType].prefixes.xprivkey + : network.prefixes.xprivkey, + depth: 0, + parentFingerPrint: new Buffer([0, 0, 0, 0]), + childIndex: 0, + chainCode: hash.slice(32, 64), + privateKey: hash.slice(0, 32) + }; +}; + +HDPrivateKey.fromSeed = function fromSeed(options, networkType) { + var seed, key; + + if (!options) + options = {}; + + seed = (options instanceof HDSeed) + ? options + : new HDSeed(options); + + key = new HDPrivateKey({ + data: HDPrivateKey._fromSeed(seed, networkType) + }); + + key.seed = seed; + + return key; +}; + +HDPrivateKey._generate = function _generate(options, networkType) { + if (!options) + opitons = {}; + + if (Buffer.isBuffer(options)) + options = { privateKey: options }; + + if (!options.privateKey) + options.privateKey = ec.generatePrivateKey(); + + if (!options.entropy) + options.entropy = ec.random(32); + + return { + version: networkType + ? network[networkType].prefixes.xprivkey + : network.prefixes.xprivkey, + depth: 0, + parentFingerPrint: new Buffer([0, 0, 0, 0]), + childIndex: 0, + chainCode: entropy, + privateKey: privateKey + }; +}; + +HDPrivateKey.generate = function generate(options, networkType) { + return new HDPrivateKey({ + data: HDPrivateKey._generate(options, networkType) + }); +}; + +HDPrivateKey.parse = function parse(xkey) { + var raw = utils.fromBase58(xkey); + var p = new BufferReader(raw, true); + var data = {}; + var i, type, prefix; + + data.version = p.readU32BE(); + data.depth = p.readU8(); + data.parentFingerPrint = p.readBytes(4); + data.childIndex = p.readU32BE(); + data.chainCode = p.readBytes(32); + p.readU8(); + data.privateKey = p.readBytes(32); + p.verifyChecksum(); + + for (i = 0; i < network.types.length; i++) { + type = network.types[i]; + prefix = network[type].prefixes.xprivkey; + if (data.version === prefix) + break; + } + + assert(i < network.types.length, 'Network not found.'); + + return { + networkType: type, + xprivkey: xkey, + data: data + }; +}; + +HDPrivateKey.fromBase58 = function fromBase58(xkey) { + var data = HDPrivateKey.parse(xkey); + return new HDPrivateKey(data); +}; + +HDPrivateKey.render = function render(data) { + var p = new BufferWriter(); + p.writeU32BE(data.version); + p.writeU8(data.depth); + p.writeBytes(data.parentFingerPrint); + p.writeU32BE(data.childIndex); + p.writeBytes(data.chainCode); + p.writeU8(0); + p.writeBytes(data.privateKey); + p.writeChecksum(); + return utils.toBase58(p.render()); +}; + HDPrivateKey.prototype.toJSON = function toJSON(passphrase) { var json = { v: 1, @@ -647,27 +614,31 @@ HDPrivateKey._fromJSON = function _fromJSON(json, passphrase) { }; HDPrivateKey.fromJSON = function fromJSON(json, passphrase) { + var key; + json = HDPrivateKey._fromJSON(json, passphrase); if (json.xprivkey) { - return new HDPrivateKey({ - xkey: json.xprivkey, - seed: json.seed ? new HDSeed(json.seed) : null - }); + key = HDPrivateKey.fromBase58(json.xprivkey); + key.seed = json.seed ? new HDSeed(json.seed) : null; + return key; } if (json.seed) return HDPrivateKey.fromSeed(json.seed); - if (json.xpubkey) { - return new HDPublicKey({ - xkey: json.xpubkey - }); - } + if (json.xpubkey) + return HDPublicKey.fromBase58(json.xprivkey); + + assert(false, 'Could not handle HD key JSON.'); +}; + +HDPublicKey.isHDPrivateKey = function isHDPrivateKey(obj) { + return obj && obj.isPrivate && typeof obj.derive === 'function'; }; /** - * HD Public Key + * HDPublicKey */ function HDPublicKey(options) { @@ -676,9 +647,7 @@ function HDPublicKey(options) { if (!(this instanceof HDPublicKey)) return new HDPublicKey(options); - if (!options) - throw new Error('No options for HDPublicKey'); - + assert(options, 'No options for HDPublicKey'); assert(!(options instanceof HDPrivateKey)); assert(!(options instanceof HDPublicKey)); @@ -694,120 +663,27 @@ function HDPublicKey(options) { if (HDPrivateKey.isExtended(options.xkey)) throw new Error('Cannot pass xprivkey into HDPublicKey'); - this.network = options.network || network.type; + this.networkType = options.networkType || network.type; + this.xpubkey = options.xkey; - data = options.xkey - ? this._unbuild(options.xkey) - : options.data; - assert(data); - - data = this._normalize(data); - - this.data = data; - - this._build(data); - - if (utils.readU32BE(data.parentFingerPrint) === 0) - this.isMaster = true; - else - this.isMaster = false; - - this.isPrivate = false; - this.isPublic = true; -} - -utils.inherits(HDPublicKey, HD); - -HDPublicKey.isHDPublicKey = function isHDPublicKey(obj) { - return obj && obj.isPublic && typeof obj._unbuild === 'function'; -}; - -HDPublicKey.prototype.deriveAccount44 = HDPrivateKey.prototype.deriveAccount44; -HDPublicKey.prototype.derivePurpose45 = HDPrivateKey.prototype.derivePurpose45; -HDPublicKey.prototype.isPurpose45 = HDPrivateKey.prototype.isPurpose45; -HDPublicKey.prototype.isAccount44 = HDPrivateKey.prototype.isAccount44; -HDPublicKey.prototype.toJSON = HDPrivateKey.prototype.toJSON; -HDPublicKey.fromJSON = HDPrivateKey.fromJSON; - -HDPublicKey.isExtended = function isExtended(data) { - if (typeof data !== 'string') - return false; - - return network.xpubkeys[data.slice(0, 4)]; -}; - -HDPublicKey.prototype._normalize = HDPrivateKey.prototype._normalize; - -HDPublicKey.prototype._unbuild = function _unbuild(xkey) { - var raw = utils.fromBase58(xkey); - var data = {}; - var off = 0; - var hash; - - data.version = raw.slice(off, off + 4); - off += 4; - data.depth = raw.slice(off, off + 1); - off += 1; - data.parentFingerPrint = raw.slice(off, off + 4); - off += 4; - data.childIndex = raw.slice(off, off + 4); - off += 4; - data.chainCode = raw.slice(off, off + 32); - off += 32; - data.publicKey = raw.slice(off, off + 33); - off += 33; - data.checksum = raw.slice(off, off + 4); - off += 4; - - hash = utils.dsha256(raw.slice(0, -4)).slice(0, 4); - if (!utils.isEqual(data.checksum, hash)) - throw new Error('checksum mismatch'); - - network.types.some(function(type) { - if (utils.readU32BE(data.version) === network[type].prefixes.xprivkey) { - this.network = type; - return true; - } - return false; - }, this); - - this.xpubkey = xkey; - - return data; -}; - -HDPublicKey.prototype._build = function _build(data) { - var off = 0; - var sequence, checksum; - - if (!this.xpubkey) { - sequence = new Buffer(82); - off += utils.copy(data.version, sequence, off); - off += utils.copy(data.depth, sequence, off); - off += utils.copy(data.parentFingerPrint, sequence, off); - off += utils.copy(data.childIndex, sequence, off); - off += utils.copy(data.chainCode, sequence, off); - off += utils.copy(data.publicKey, sequence, off); - assert(off === 78, off); - checksum = utils.dsha256(sequence.slice(0, off)).slice(0, 4); - off += utils.copy(checksum, sequence, off); - assert(off === 82, off); - - if (!data.checksum || !data.checksum.length) - data.checksum = checksum; - else if (utils.toHex(checksum) !== utils.toHex(data.checksum)) - throw new Error('checksum mismatch'); - - this.xpubkey = utils.toBase58(sequence); + if (options.data) { + data = options.data; + } else if (options.xkey) { + data = HDPublicKey.parse(options.xkey); + this.networkType = data.networkType; + data = data.data; + } else { + assert(false, 'No data passed to HD key.'); } + assert(data.depth <= 0xff, 'Depth is too high.'); + this.version = data.version; this.depth = data.depth; this.parentFingerPrint = data.parentFingerPrint; this.childIndex = data.childIndex; this.chainCode = data.chainCode; this.publicKey = data.publicKey; - this.checksum = null; this.privateKey = null; this.fingerPrint = null; @@ -816,11 +692,18 @@ HDPublicKey.prototype._build = function _build(data) { this.hdPrivateKey = null; this.xprivkey = null; -}; + + if (!this.xpubkey) + this.xpubkey = HDPublicKey.render(data); + + this.isPrivate = false; + this.isPublic = true; +} + +utils.inherits(HDPublicKey, HD); HDPublicKey.prototype.derive = function derive(index, hardened) { - var off = 0; - var cached, data, hash, leftPart, chainCode; + var cached, p, data, hash, leftPart, chainCode; var publicPoint, point, publicKey, child; if (typeof index === 'string') @@ -832,14 +715,15 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { return cached; if (index >= constants.hd.hardened || hardened) - throw new Error('invalid index'); + throw new Error('Invalid index.'); if (index < 0) - throw new Error('invalid path'); + throw new Error('Invalid path.'); - data = new Buffer(this.publicKey.length + 4); - off += utils.copy(this.publicKey, data, off); - off += utils.writeU32BE(data, index, off); + p = new BufferWriter(); + p.writeBytes(this.publicKey); + p.writeU32BE(index); + data = p.render(); hash = utils.sha512hmac(data, this.chainCode); leftPart = new bn(hash.slice(0, 32)); @@ -855,15 +739,14 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { } child = new HDPublicKey({ - network: this.network, + networkType: this.networkType, data: { version: this.version, - depth: utils.readU8(this.depth) + 1, + depth: this.depth + 1, parentFingerPrint: this.fingerPrint, childIndex: index, chainCode: chainCode, - publicKey: publicKey, - checksum: null + publicKey: publicKey } }); @@ -872,9 +755,16 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { return child; }; +HDPublicKey.prototype.deriveAccount44 = HDPrivateKey.prototype.deriveAccount44; +HDPublicKey.prototype.derivePurpose45 = HDPrivateKey.prototype.derivePurpose45; +HDPublicKey.prototype.isPurpose45 = HDPrivateKey.prototype.isPurpose45; +HDPublicKey.prototype.isAccount44 = HDPrivateKey.prototype.isAccount44; + HDPublicKey.isValidPath = function isValidPath(arg) { + var indexes; + if (typeof arg === 'string') { - var indexes = HDPrivateKey._getIndexes(arg); + indexes = HDPrivateKey._getIndexes(arg); return indexes !== null && indexes.every(HDPublicKey.isValidPath); } @@ -885,20 +775,84 @@ HDPublicKey.isValidPath = function isValidPath(arg) { }; HDPublicKey.prototype.derivePath = function derivePath(path) { - if (~path.indexOf('\'')) - throw new Error('cannot derive hardened'); - else if (!HDPublicKey.isValidPath(path)) - throw new Error('invalid path'); + var indexes; - var indexes = HDPrivateKey._getIndexes(path); + if (path.indexOf('\'') !== -1) + throw new Error('Cannot derive hardened.'); + + if (!HDPublicKey.isValidPath(path)) + throw new Error('Invalid path.'); + + indexes = HDPrivateKey._getIndexes(path); return indexes.reduce(function(prev, index) { return prev.derive(index); }, this); }; +HDPublicKey.prototype.toJSON = HDPrivateKey.prototype.toJSON; +HDPublicKey.fromJSON = HDPrivateKey.fromJSON; + +HDPublicKey.isExtended = function isExtended(data) { + if (typeof data !== 'string') + return false; + + return network.xpubkeys[data.slice(0, 4)]; +}; + +HDPublicKey.parse = function parse(xkey) { + var raw = utils.fromBase58(xkey); + var p = new BufferReader(raw, true); + var data = {}; + var i, type, prefix; + + data.version = p.readU32BE(); + data.depth = p.readU8(); + data.parentFingerPrint = p.readBytes(4); + data.childIndex = p.readU32BE(); + data.chainCode = p.readBytes(32); + data.publicKey = p.readBytes(33); + p.verifyChecksum(); + + for (i = 0; i < network.types.length; i++) { + type = network.types[i]; + prefix = network[type].prefixes.xpubkey; + if (data.version === prefix) + break; + } + + assert(i < network.types.length, 'Network not found.'); + + return { + networkType: type, + xpubkey: xkey, + data: data + }; +}; + +HDPublicKey.render = function render(data) { + var p = new BufferWriter(); + p.writeU32BE(data.version); + p.writeU8(data.depth); + p.writeBytes(data.parentFingerPrint); + p.writeU32BE(data.childIndex); + p.writeBytes(data.chainCode); + p.writeBytes(data.publicKey); + p.writeChecksum(); + return utils.toBase58(p.render()); +}; + +HDPublicKey.fromBase58 = function fromBase58(xkey) { + var data = HDPublicKey.parse(xkey); + return new HDPublicKey(data); +}; + +HDPublicKey.isHDPublicKey = function isHDPublicKey(obj) { + return obj && obj.isPublic && typeof obj.derive === 'function'; +}; + /** - * Make HD keys behave like elliptic KeyPairs + * Make HD keys behave like KeyPairs */ [HDPrivateKey, HDPublicKey].forEach(function(HD) { @@ -925,16 +879,6 @@ HDPrivateKey.prototype.toSecret = function toSecret() { return KeyPair.toSecret.call(this); }; -/** - * Helpers - */ - -function array32(data) { - var b = new Buffer(4); - utils.writeU32BE(b, data, 0); - return b; -} - /** * Expose */ diff --git a/lib/bcoin/reader.js b/lib/bcoin/reader.js index 1104f69d..34e7f8f9 100644 --- a/lib/bcoin/reader.js +++ b/lib/bcoin/reader.js @@ -283,6 +283,21 @@ BufferReader.prototype.seek = function seek(off) { return off; }; +BufferReader.prototype.createChecksum = function createChecksum() { + assert(this.offset + 4 >= 0); + assert(this.offset + 4 <= this.data.length); + var start = this.stack[this.stack.length - 1] || 0; + var data = this.data.slice(start, this.offset); + return utils.readU32BE(utils.checksum(data), 0); +}; + +BufferReader.prototype.verifyChecksum = function verifyChecksum() { + var chk = this.createChecksum(); + var checksum = this.readU32BE(); + assert(chk === checksum, 'Checksum mismatch.'); + return checksum; +}; + /** * Expose */ diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index ab2b678f..4a5243df 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -36,7 +36,7 @@ function Wallet(options) { } if (!options.master) - options.master = bcoin.hd.fromSeed(); + options.master = bcoin.hd.privateKey.fromSeed(); this.options = options; this.provider = options.provider || null; @@ -188,9 +188,9 @@ Wallet.prototype.addKey = function addKey(key) { } if (bcoin.hd.privateKey.isExtended(key)) - key = bcoin.hd.privateKey(key); + key = bcoin.hd.privateKey.fromBase58(key); else if (bcoin.hd.publicKey.isExtended(key)) - key = bcoin.hd.publicKey(key); + key = bcoin.hd.publicKey.fromBase58(key); if (key instanceof bcoin.hd.privateKey) key = key.hdPublicKey; @@ -229,9 +229,9 @@ Wallet.prototype.removeKey = function removeKey(key) { } if (bcoin.hd.privateKey.isExtended(key)) - key = bcoin.hd.privateKey(key); + key = bcoin.hd.privateKey.fromBase58(key); else if (bcoin.hd.publicKey.isExtended(key)) - key = bcoin.hd.publicKey(key); + key = bcoin.hd.publicKey.fromBase58(key); if (key instanceof bcoin.hd.privateKey) key = key.hdPublicKey; diff --git a/lib/bcoin/writer.js b/lib/bcoin/writer.js index 4ca054bb..6f9b7621 100644 --- a/lib/bcoin/writer.js +++ b/lib/bcoin/writer.js @@ -46,6 +46,7 @@ BufferWriter.prototype.render = function render(keep) { case '64be': off += utils.write64BE(data, item[1], off); break; case 'varint': off += utils.writeVarint(data, item[1], off); break; case 'bytes': off += utils.copy(item[1], data, off); break; + case 'checksum': off += utils.copy(utils.checksum(data.slice(0, off)), data, off); break; } } @@ -176,6 +177,11 @@ BufferWriter.prototype.writeVarint = function writeVarint(value) { this.data.push(['varint', value]); }; +BufferWriter.prototype.writeChecksum = function writeChecksum() { + this.written += 4; + this.data.push(['checksum']); +}; + BufferWriter.prototype.fill = function fill(value, size) { assert(size >= 0); var buf = new Buffer(size); diff --git a/test/hd-test.js b/test/hd-test.js index 19454ef5..80c0abfe 100644 --- a/test/hd-test.js +++ b/test/hd-test.js @@ -37,7 +37,9 @@ describe('HD', function() { }); it('should create master private key', function() { - master = bcoin.hd.priv({ seed: seed }); + var s = new bcoin.hd.seed(); + s.seed = new Buffer(seed, 'hex'); + master = bcoin.hd.priv.fromSeed(s); assert.equal(master.xprivkey, master_priv); assert.equal(master.xpubkey, master_pub); }); @@ -78,11 +80,11 @@ describe('HD', function() { }); it('should deserialize master private key', function() { - master._unbuild(master.xprivkey); + bcoin.hd.priv.parse(master.xprivkey); }); it('should deserialize master public key', function() { - master.hdPublicKey._unbuild(master.hdPublicKey.xpubkey); + bcoin.hd.pub.parse(master.hdPublicKey.xpubkey); }); it('should deserialize and reserialize', function() {