From 950ea6ed1a378b7a1e5a3fe8323207b563af9110 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 26 Nov 2014 18:38:15 -0300 Subject: [PATCH] Add public key --- index.js | 3 +- lib/bip32.js | 320 --------------------- lib/{hdprivkey.js => hdprivatekey.js} | 74 ++--- lib/hdpublickey.js | 399 ++++++++++++++++++++++++++ lib/networks.js | 1 + lib/privatekey.js | 45 +-- lib/publickey.js | 2 - lib/util.js | 16 ++ test/hdkeys.js | 12 +- 9 files changed, 485 insertions(+), 387 deletions(-) delete mode 100644 lib/bip32.js rename lib/{hdprivkey.js => hdprivatekey.js} (85%) create mode 100644 lib/hdpublickey.js diff --git a/index.js b/index.js index 07e1f7747..ec211ddc9 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,8 @@ bitcore.encoding.Varint = require('./lib/encoding/varint'); bitcore.Address = require('./lib/address'); bitcore.Block = require('./lib/block'); bitcore.Blockheader = require('./lib/blockheader'); -bitcore.HDPrivateKey = require('./lib/hdprivkey.js'); +bitcore.HDPrivateKey = require('./lib/hdprivatekey.js'); +bitcore.HDPublicKey = require('./lib/hdpublickey.js'); bitcore.Networks = require('./lib/networks'); bitcore.Opcode = require('./lib/opcode'); bitcore.PrivateKey = require('./lib/privatekey'); diff --git a/lib/bip32.js b/lib/bip32.js deleted file mode 100644 index d27c46161..000000000 --- a/lib/bip32.js +++ /dev/null @@ -1,320 +0,0 @@ -'use strict'; - -var Base58Check = require('./encoding/base58check'); -var networks = require('./networks'); -var Hash = require('./crypto/hash'); -var Point = require('./crypto/point'); -var Random = require('./crypto/random'); -var BN = require('./crypto/bn'); -var PublicKey = require('./publickey'); -var PrivateKey = require('./privatekey'); - -var BIP32 = function BIP32(obj) { - if (!(this instanceof BIP32)) - return new BIP32(obj); - if (typeof obj === 'string') { - var str = obj; - this.fromString(str); - } else if (obj ) { - this.set(obj); - } -}; - -BIP32.prototype.set = function(obj) { - this.version = typeof obj.version !== 'undefined' ? obj.version : this.version; - this.depth = typeof obj.depth !== 'undefined' ? obj.depth : this.depth; - this.parentfingerprint = obj.parentfingerprint || this.parentfingerprint; - this.childindex = obj.childindex || this.childindex; - this.chaincode = obj.chaincode || this.chaincode; - this.hasprivkey = typeof obj.hasprivkey !== 'undefined' ? obj.hasprivkey : this.hasprivkey; - this.pubkeyhash = obj.pubkeyhash || this.pubkeyhash; - this.xpubkey = obj.xpubkey || this.xpubkey; - this.xprivkey = obj.xprivkey || this.xprivkey; - return this; -}; - -BIP32.prototype.fromRandom = function(networkstr) { - if (!networkstr) - networkstr = 'mainnet'; - this.version = networks[networkstr].xprivkey; - this.depth = 0x00; - this.parentfingerprint = new Buffer([0, 0, 0, 0]); - this.childindex = new Buffer([0, 0, 0, 0]); - this.chaincode = Random.getRandomBuffer(32); - this.privkey = PrivateKey.fromRandom(); - this.pubkey = PublicKey.fromPrivateKey(this.privkey); - this.hasprivkey = true; - this.pubkeyhash = Hash.sha256ripemd160(this.pubkey.toBuffer()); - this.buildxpubkey(); - this.buildxprivkey(); -}; - -BIP32.prototype.fromString = function(str) { - var bytes = Base58Check.decode(str); - this.initFromBytes(bytes); - return this; -}; - -BIP32.prototype.fromSeed = function(bytes, networkstr) { - if (!networkstr) - networkstr = 'mainnet'; - - if (!Buffer.isBuffer(bytes)) - throw new Error('bytes must be a buffer'); - if (bytes.length < 128 / 8) - throw new Error('Need more than 128 bytes of entropy'); - if (bytes.length > 512 / 8) - throw new Error('More than 512 bytes of entropy is nonstandard'); - var hash = Hash.sha512hmac(bytes, new Buffer('Bitcoin seed')); - - this.depth = 0x00; - this.parentfingerprint = new Buffer([0, 0, 0, 0]); - this.childindex = new Buffer([0, 0, 0, 0]); - this.chaincode = hash.slice(32, 64); - this.version = networks[networkstr].xprivkey; - this.privkey = new PrivateKey(BN().fromBuffer(hash.slice(0, 32))); - this.pubkey = PublicKey.fromPrivateKey(this.privkey); - this.hasprivkey = true; - this.pubkeyhash = Hash.sha256ripemd160(this.pubkey.toBuffer()); - - this.buildxpubkey(); - this.buildxprivkey(); - - return this; -}; - -BIP32.prototype.initFromBytes = function(bytes) { - // Both pub and private extended keys are 78 bytes - if (bytes.length != 78) - throw new Error('not enough data'); - - this.version = bytes.slice(0, 4).readUInt32BE(0); - this.depth = bytes.slice(4, 5).readUInt8(0); - this.parentfingerprint = bytes.slice(5, 9); - this.childindex = bytes.slice(9, 13).readUInt32BE(0); - this.chaincode = bytes.slice(13, 45); - - var keyBytes = bytes.slice(45, 78); - - var isPrivate = - (this.version == networks.mainnet.xprivkey || - this.version == networks.testnet.xprivkey); - - var isPublic = - (this.version == networks.mainnet.xpubkey || - this.version == networks.testnet.xpubkey); - - if (isPrivate && keyBytes[0] == 0) { - this.privkey = new PrivateKey(BN().fromBuffer(keyBytes.slice(1, 33))); - this.pubkey = PublicKey.fromPrivateKey(this.privkey); - this.pubkeyhash = Hash.sha256ripemd160(this.pubkey.toBuffer()); - this.hasprivkey = true; - } else if (isPublic && (keyBytes[0] == 0x02 || keyBytes[0] == 0x03)) { - this.pubkey = PublicKey.fromDER(keyBytes); - this.pubkeyhash = Hash.sha256ripemd160(this.pubkey.toBuffer()); - this.hasprivkey = false; - } else { - throw new Error('Invalid key'); - } - - this.buildxpubkey(); - this.buildxprivkey(); -}; - -BIP32.prototype.buildxpubkey = function() { - this.xpubkey = new Buffer([]); - - var v = null; - switch (this.version) { - case networks.mainnet.xpubkey: - case networks.mainnet.xprivkey: - v = networks.mainnet.xpubkey; - break; - case networks.testnet.xpubkey: - case networks.testnet.xprivkey: - v = networks.testnet.xpubkey; - break; - default: - throw new Error('Unknown version'); - } - - // Version - this.xpubkey = Buffer.concat([ - new Buffer([v >> 24]), - new Buffer([(v >> 16) & 0xff]), - new Buffer([(v >> 8) & 0xff]), - new Buffer([v & 0xff]), - new Buffer([this.depth]), - this.parentfingerprint, - new Buffer([this.childindex >>> 24]), - new Buffer([(this.childindex >>> 16) & 0xff]), - new Buffer([(this.childindex >>> 8) & 0xff]), - new Buffer([this.childindex & 0xff]), - this.chaincode, - this.pubkey.toBuffer() - ]); -}; - -BIP32.prototype.xpubkeyString = function(format) { - if (format === undefined || format === 'base58') { - return Base58Check.encode(this.xpubkey); - } else if (format === 'hex') { - return this.xpubkey.toString('hex'); - } else { - throw new Error('bad format'); - } -} - -BIP32.prototype.buildxprivkey = function() { - if (!this.hasprivkey) return; - this.xprivkey = new Buffer([]); - - var v = this.version; - - this.xprivkey = Buffer.concat([ - new Buffer([v >> 24]), - new Buffer([(v >> 16) & 0xff]), - new Buffer([(v >> 8) & 0xff]), - new Buffer([v & 0xff]), - new Buffer([this.depth]), - this.parentfingerprint, - new Buffer([this.childindex >>> 24]), - new Buffer([(this.childindex >>> 16) & 0xff]), - new Buffer([(this.childindex >>> 8) & 0xff]), - new Buffer([this.childindex & 0xff]), - this.chaincode, - new Buffer([0]), - this.privkey.bn.toBuffer({size: 32}) - ]); -} - -BIP32.prototype.xprivkeyString = function(format) { - if (format === undefined || format === 'base58') { - return Base58Check.encode(this.xprivkey); - } else if (format === 'hex') { - return this.xprivkey.toString('hex'); - } else { - throw new Error('bad format'); - } -} - - -BIP32.prototype.derive = function(path) { - var e = path.split('/'); - - // Special cases: - if (path == 'm' || path == 'M' || path == 'm\'' || path == 'M\'') - return this; - - var bip32 = this; - for (var i in e) { - var c = e[i]; - - if (i == 0) { - if (c != 'm') throw new Error('invalid path'); - continue; - } - - if (parseInt(c.replace("'", "")).toString() !== c.replace("'", "")) - throw new Error('invalid path'); - - var usePrivate = (c.length > 1) && (c[c.length - 1] == '\''); - var childindex = parseInt(usePrivate ? c.slice(0, c.length - 1) : c) & 0x7fffffff; - - if (usePrivate) - childindex += 0x80000000; - - bip32 = bip32.deriveChild(childindex); - } - - return bip32; -}; - -BIP32.prototype.deriveChild = function(i) { - if (typeof i !== 'number') - throw new Error('i must be a number'); - - var ib = []; - ib.push((i >> 24) & 0xff); - ib.push((i >> 16) & 0xff); - ib.push((i >> 8) & 0xff); - ib.push(i & 0xff); - ib = new Buffer(ib); - - var usePrivate = (i & 0x80000000) != 0; - - var isPrivate = - (this.version == networks.mainnet.xprivkey || - this.version == networks.testnet.xprivkey); - - if (usePrivate && (!this.hasprivkey || !isPrivate)) - throw new Error('Cannot do private key derivation without private key'); - - var ret = null; - if (this.hasprivkey) { - var data = null; - - if (usePrivate) { - data = Buffer.concat([new Buffer([0]), this.privkey.bn.toBuffer({size: 32}), ib]); - } else { - data = Buffer.concat([this.pubkey.toBuffer({size: 32}), ib]); - } - - var hash = Hash.sha512hmac(data, this.chaincode); - var il = BN().fromBuffer(hash.slice(0, 32), {size: 32}); - var ir = hash.slice(32, 64); - - // ki = IL + kpar (mod n). - var k = il.add(this.privkey.bn).mod(Point.getN()); - - ret = new BIP32(); - ret.chaincode = ir; - - ret.privkey = new PrivateKey(k); - ret.pubkey = PublicKey.fromPrivateKey(ret.privkey); - ret.hasprivkey = true; - - } else { - var data = Buffer.concat([this.pubkey.toBuffer(), ib]); - var hash = Hash.sha512hmac(data, this.chaincode); - var il = BN().fromBuffer(hash.slice(0, 32)); - var ir = hash.slice(32, 64); - - // Ki = (IL + kpar)*G = IL*G + Kpar - var ilG = Point.getG().mul(il); - var Kpar = this.pubkey.point; - var Ki = ilG.add(Kpar); - var newpub = PublicKey.fromPoint(Ki); - - ret = new BIP32(); - ret.chaincode = ir; - - ret.pubkey = newpub; - ret.hasprivkey = false; - } - - ret.childindex = i; - ret.parentfingerprint = this.pubkeyhash.slice(0, 4); - ret.version = this.version; - ret.depth = this.depth + 1; - - ret.pubkeyhash = Hash.sha256ripemd160(ret.pubkey.toBuffer()); - - ret.buildxpubkey(); - ret.buildxprivkey(); - - return ret; -}; - -BIP32.prototype.toString = function() { - var isPrivate = - (this.version == networks.mainnet.xprivkey || - this.version == networks.testnet.xprivkey); - - if (isPrivate) - return this.xprivkeyString(); - else - return this.xpubkeyString(); -}; - -module.exports = BIP32; diff --git a/lib/hdprivkey.js b/lib/hdprivatekey.js similarity index 85% rename from lib/hdprivkey.js rename to lib/hdprivatekey.js index 0006a618f..cd29b580e 100644 --- a/lib/hdprivkey.js +++ b/lib/hdprivatekey.js @@ -58,10 +58,13 @@ HDPrivateKey.prototype.derive = function(arg, hardened) { } }; -HDPrivateKey.prototype._deriveWithNumber = function deriveWithNumber(index, hardened) { +HDPrivateKey.prototype._deriveWithNumber = function(index, hardened) { if (index >= HDPrivateKey.Hardened) { hardened = true; } + if (index < HDPrivateKey.Hardened && hardened) { + index += HDPrivateKey.Hardened; + } var indexBuffer = util.integerAsBuffer(index); var data; @@ -75,7 +78,6 @@ HDPrivateKey.prototype._deriveWithNumber = function deriveWithNumber(index, hard var chainCode = hash.slice(32, 64); var privateKey = leftPart.add(this.privateKey.toBigNumber()).mod(Point.getN()).toBuffer({size: 32}); - console.log(privateKey); return new HDPrivateKey({ network: this.network, @@ -87,7 +89,7 @@ HDPrivateKey.prototype._deriveWithNumber = function deriveWithNumber(index, hard }); }; -HDPrivateKey.prototype._deriveFromString = function deriveFromString(path) { +HDPrivateKey.prototype._deriveFromString = function(path) { var steps = path.split('/'); // Special cases: @@ -101,9 +103,9 @@ HDPrivateKey.prototype._deriveFromString = function deriveFromString(path) { var result = this; for (var step in steps) { - var index = parseInt(step); - var hardened = step !== index.toString(); - result = result.derive(index, hardened); + var index = parseInt(steps[step]); + var hardened = steps[step] !== index.toString(); + result = result._deriveWithNumber(index, hardened); } return result; }; @@ -117,7 +119,7 @@ HDPrivateKey.prototype._deriveFromString = function deriveFromString(path) { * network provided matches the network serialized. * @return {boolean} */ -HDPrivateKey.isValidSerialized = function isValidSerialized(data, network) { +HDPrivateKey.isValidSerialized = function(data, network) { return !HDPrivateKey.getSerializedError(data, network); }; @@ -130,7 +132,7 @@ HDPrivateKey.isValidSerialized = function isValidSerialized(data, network) { * network provided matches the network serialized. * @return {HDPrivateKey.Errors|null} */ -HDPrivateKey.getSerializedError = function getSerializedError(data, network) { +HDPrivateKey.getSerializedError = function(data, network) { /* jshint maxcomplexity: 10 */ if (!(_.isString(data) || buffer.Buffer.isBuffer(data))) { return HDPrivateKey.Errors.InvalidArgument; @@ -155,7 +157,7 @@ HDPrivateKey.getSerializedError = function getSerializedError(data, network) { return null; }; -HDPrivateKey._validateNetwork = function validateNetwork(data, network) { +HDPrivateKey._validateNetwork = function(data, network) { network = Network.get(network); if (!network) { return HDPrivateKey.Errors.InvalidNetworkArgument; @@ -167,17 +169,17 @@ HDPrivateKey._validateNetwork = function validateNetwork(data, network) { return null; }; -HDPrivateKey.prototype._buildFromJson = function buildFromJson(arg) { +HDPrivateKey.prototype._buildFromJson = function(arg) { return this._buildFromObject(JSON.parse(arg)); }; -HDPrivateKey.prototype._buildFromObject = function buildFromObject(arg) { +HDPrivateKey.prototype._buildFromObject = function(arg) { // TODO: Type validation var buffers = { version: util.integerAsBuffer(Network.get(arg.network).xprivkey), depth: util.integerAsSingleByteBuffer(arg.depth), - parentFingerPrint: util.integerAsBuffer(arg.parentFingerPrint), - childIndex: util.integerAsBuffer(arg.childIndex), + parentFingerPrint: _.isNumber(arg.parentFingerPrint) ? util.integerAsBuffer(arg.parentFingerPrint) : arg.parentFingerPrint, + childIndex: _.isNumber(arg.childIndex) ? util.integerAsBuffer(arg.childIndex) : arg.childIndex, chainCode: _.isString(arg.chainCode) ? util.hexToBuffer(arg.chainCode) : arg.chainCode, privateKey: _.isString(arg.privateKey) ? util.hexToBuffer(arg.privateKey) : arg.privateKey, checksum: arg.checksum && arg.checksum.length ? util.integerAsBuffer(arg.checksum) : undefined @@ -185,7 +187,7 @@ HDPrivateKey.prototype._buildFromObject = function buildFromObject(arg) { return this._buildFromBuffers(buffers); }; -HDPrivateKey.prototype._buildFromSerialized = function buildFromSerialized(arg) { +HDPrivateKey.prototype._buildFromSerialized = function(arg) { var decoded = Base58Check.decode(arg); var buffers = { version: decoded.slice(HDPrivateKey.VersionStart, HDPrivateKey.VersionEnd), @@ -201,11 +203,11 @@ HDPrivateKey.prototype._buildFromSerialized = function buildFromSerialized(arg) return this._buildFromBuffers(buffers); }; -HDPrivateKey.prototype._generateRandomly = function generateRandomly(network) { - return HDPrivateKey.fromSeed(Random.getRandomBytes(64), network); +HDPrivateKey.prototype._generateRandomly = function(network) { + return HDPrivateKey.fromSeed(Random.getRandomBuffer(64), network); }; -HDPrivateKey.fromSeed = function fromSeed(hexa, network) { +HDPrivateKey.fromSeed = function(hexa, network) { /* jshint maxcomplexity: 8 */ if (util.isHexaString(hexa)) { @@ -248,20 +250,17 @@ HDPrivateKey.fromSeed = function fromSeed(hexa, network) { * representation * @return {HDPrivateKey} this */ -HDPrivateKey.prototype._buildFromBuffers = function buildFromBuffers(arg) { +HDPrivateKey.prototype._buildFromBuffers = function(arg) { /* jshint maxcomplexity: 8 */ + /* jshint maxstatements: 20 */ - console.log(arg.privateKey); HDPrivateKey._validateBufferArguments(arg); this._buffers = arg; - console.log(arg.privateKey); var sequence = [ arg.version, arg.depth, arg.parentFingerPrint, arg.childIndex, arg.chainCode, util.emptyBuffer(1), arg.privateKey ]; - console.log(arg.privateKey); - console.log(sequence); if (!arg.checksum || !arg.checksum.length) { arg.checksum = Base58Check.checksum(buffer.Buffer.concat(sequence)); } else { @@ -271,24 +270,25 @@ HDPrivateKey.prototype._buildFromBuffers = function buildFromBuffers(arg) { } if (!arg.xprivkey) { - sequence.push(arg.checksum); - this.xprivkey = Base58.encode(buffer.Buffer.concat(sequence)); + this.xprivkey = Base58Check.encode(buffer.Buffer.concat(sequence)); } else { this.xprivkey = arg.xprivkey; } - - // TODO: - // * Instantiate associated HDPublicKey - this.network = Network.get(util.integerFromBuffer(arg.version)); - this.privateKey = new PrivateKey(arg.privateKey); - this.publicKey = this.privateKey.publicKey; - this.fingerPrint = Base58Check.checksum(util.hexToBuffer(this.publicKey.toString())); + this.depth = util.integerFromSingleByteBuffer(arg.depth); + this.privateKey = new PrivateKey(BN().fromBuffer(arg.privateKey)); + this.publicKey = this.privateKey.toPublicKey(); + + this.fingerPrint = Hash.sha256ripemd160(this.publicKey.toBuffer()).slice(0, HDPrivateKey.ParentFingerPrintSize); + + var HDPublicKey = require('./hdpublickey'); + this.hdPublicKey = new HDPublicKey(this); + this.xpubkey = this.hdPublicKey.xpubkey; return this; }; -HDPrivateKey._validateBufferArguments = function validateBufferArguments(arg) { +HDPrivateKey._validateBufferArguments = function(arg) { var checkBuffer = function(name, size) { var buff = arg[name]; assert(buffer.Buffer.isBuffer(buff), name + ' argument is not a buffer'); @@ -308,14 +308,14 @@ HDPrivateKey._validateBufferArguments = function validateBufferArguments(arg) { } }; -HDPrivateKey.prototype.toString = function toString() { +HDPrivateKey.prototype.toString = function() { return this.xprivkey; }; -HDPrivateKey.prototype.toObject = function toObject() { +HDPrivateKey.prototype.toObject = function() { return { - network: Network.get(util.integerFromBuffer(this._buffers.version)), - depth: util.integerFromBuffer(this._buffers.depth), + network: Network.get(util.integerFromBuffer(this._buffers.version)).name, + depth: util.integerFromSingleByteBuffer(this._buffers.depth), fingerPrint: this.fingerPrint, parentFingerPrint: util.integerFromBuffer(this._buffers.parentFingerPrint), childIndex: util.integerFromBuffer(this._buffers.childIndex), @@ -326,7 +326,7 @@ HDPrivateKey.prototype.toObject = function toObject() { }; }; -HDPrivateKey.prototype.toJson = function toJson() { +HDPrivateKey.prototype.toJson = function() { return JSON.stringify(this.toObject()); }; diff --git a/lib/hdpublickey.js b/lib/hdpublickey.js new file mode 100644 index 000000000..da975ab3a --- /dev/null +++ b/lib/hdpublickey.js @@ -0,0 +1,399 @@ +'use strict'; + +var _ = require('lodash'); +var BN = require('./crypto/bn'); +var Base58 = require('./encoding/base58'); +var Base58Check = require('./encoding/base58check'); +var Hash = require('./crypto/hash'); +var HDPrivateKey = require('./hdprivatekey'); +var Network = require('./networks'); +var Point = require('./crypto/point'); +var PublicKey = require('./publickey'); +var Random = require('./crypto/random'); + +var assert = require('assert'); +var buffer = require('buffer'); +var util = require('./util'); + +var MINIMUM_ENTROPY_BITS = 128; +var BITS_TO_BYTES = 1/8; +var MAXIMUM_ENTROPY_BITS = 512; + + +function HDPublicKey(arg) { + /* jshint maxcomplexity: 12 */ + /* jshint maxstatements: 20 */ + if (arg instanceof HDPublicKey) { + return arg; + } + if (!(this instanceof HDPublicKey)) { + return new HDPublicKey(arg); + } + if (arg) { + if (_.isString(arg) || buffer.Buffer.isBuffer(arg)) { + if (HDPublicKey.isValidSerialized(arg)) { + this._buildFromSerialized(arg); + } else { + var error = HDPublicKey.getSerializedError(arg); + if (error === HDPublicKey.Errors.ArgumentIsPrivateExtended) { + return new HDPrivateKey(arg).hdPublicKey; + } + throw new Error(error); + } + } else { + if (_.isObject(arg)) { + if (arg instanceof HDPrivateKey) { + this._buildFromPrivate(arg); + } else { + this._buildFromObject(arg); + } + } else if (util.isValidJson(arg)) { + this._buildFromJson(arg); + } else { + throw new Error(HDPublicKey.Errors.UnrecognizedArgument); + } + } + } else { + this._generateRandomly(); + } +} + +HDPublicKey.prototype.derive = function (arg, hardened) { + if (_.isNumber(arg)) { + return this._deriveWithNumber(arg, hardened); + } else if (_.isString(arg)) { + return this._deriveFromString(arg); + } else { + throw new Error(HDPublicKey.Errors.InvalidDerivationArgument); + } +}; + +HDPublicKey.prototype._deriveWithNumber = function (index, hardened) { + if (hardened || index >= HDPublicKey.Hardened) { + throw new Error(HDPublicKey.Errors.InvalidIndexCantDeriveHardened); + } + + var indexBuffer = util.integerAsBuffer(index); + var data = buffer.Buffer.concat([this.publicKey.toBuffer(), indexBuffer]); + var hash = Hash.sha512hmac(data, this._buffers.chainCode); + var leftPart = BN().fromBuffer(hash.slice(0, 32), {size: 32}); + var chainCode = hash.slice(32, 64); + + var publicKey = PublicKey.fromPoint(Point.getG().mul(leftPart).add(this.publicKey.point)); + + return new HDPublicKey({ + network: this.network, + depth: this.depth + 1, + parentFingerPrint: this.fingerPrint, + childIndex: index, + chainCode: chainCode, + publicKey: publicKey + }); +}; + +HDPublicKey.prototype._deriveFromString = function (path) { + /* jshint maxcomplexity: 8 */ + var steps = path.split('/'); + + // Special cases: + if (_.contains(HDPublicKey.RootElementAlias, path)) { + return this; + } + if (!_.contains(HDPublicKey.RootElementAlias, steps[0])) { + throw new Error(HDPublicKey.Errors.InvalidPath); + } + steps = steps.slice(1); + + var result = this; + for (var step in steps) { + var index = parseInt(steps[step]); + var hardened = steps[step] !== index.toString(); + result = result._deriveWithNumber(index, hardened); + } + return result; +}; + +/** + * Verifies that a given serialized private key in base58 with checksum format + * is valid. + * + * @param {string|Buffer} data - the serialized private key + * @param {string|Network=} network - optional, if present, checks that the + * network provided matches the network serialized. + * @return {boolean} + */ +HDPublicKey.isValidSerialized = function (data, network) { + return !HDPublicKey.getSerializedError(data, network); +}; + +/** + * Checks what's the error that causes the validation of a serialized private key + * in base58 with checksum to fail. + * + * @param {string|Buffer} data - the serialized private key + * @param {string|Network=} network - optional, if present, checks that the + * network provided matches the network serialized. + * @return {HDPublicKey.Errors|null} + */ +HDPublicKey.getSerializedError = function (data, network) { + /* jshint maxcomplexity: 10 */ + network = Network.get(network) || Network.defaultNetwork; + if (!(_.isString(data) || buffer.Buffer.isBuffer(data))) { + return HDPublicKey.Errors.InvalidArgument; + } + if (!Base58.validCharacters(data)) { + return HDPublicKey.Errors.InvalidB58Char; + } + try { + data = Base58Check.decode(data); + } catch (e) { + return HDPublicKey.Errors.InvalidB58Checksum; + } + if (data.length !== 78) { + return HDPublicKey.Errors.InvalidLength; + } + if (util.integerFromBuffer(data.slice(0, 4)) === network.xprivkey) { + return HDPublicKey.Errors.ArgumentIsPrivateExtended; + } + if (!_.isUndefined(network)) { + var error = HDPublicKey._validateNetwork(data, network); + if (error) { + return error; + } + } + return null; +}; + +HDPublicKey._validateNetwork = function (data, network) { + network = Network.get(network); + if (!network) { + return HDPublicKey.Errors.InvalidNetworkArgument; + } + var version = data.slice(HDPublicKey.VersionStart, HDPublicKey.VersionEnd); + if (util.integerFromBuffer(version) !== network.xpubkey) { + return HDPublicKey.Errors.InvalidNetwork; + } + return null; +}; + +HDPublicKey.prototype._buildFromJson = function (arg) { + return this._buildFromObject(JSON.parse(arg)); +}; + +HDPublicKey.prototype._buildFromPrivate = function (arg) { + var args = _.clone(arg._buffers); + var point = Point.getG().mul(BN().fromBuffer(args.privateKey)); + args.publicKey = util.pointToCompressed(point); + args.version = util.integerAsBuffer(Network.get(util.integerFromBuffer(args.version)).xpubkey); + args.privateKey = undefined; + args.checksum = undefined; + args.xprivkey = undefined; + return this._buildFromBuffers(args); +}; + +HDPublicKey.prototype._buildFromObject = function (arg) { + /* jshint maxcomplexity: 8 */ + // TODO: Type validation + var buffers = { + version: util.integerAsBuffer(Network.get(arg.network).xpubkey), + depth: util.integerAsSingleByteBuffer(arg.depth), + parentFingerPrint: _.isNumber(arg.parentFingerPrint) ? util.integerAsBuffer(arg.parentFingerPrint) : arg.parentFingerPrint, + childIndex: util.integerAsBuffer(arg.childIndex), + chainCode: _.isString(arg.chainCode) ? util.hexToBuffer(arg.chainCode) : arg.chainCode, + publicKey: _.isString(arg.publicKey) ? util.hexToBuffer(arg.publicKey) : + buffer.Buffer.isBuffer(arg.publicKey) ? arg.publicKey : arg.publicKey.toBuffer(), + checksum: arg.checksum && arg.checksum.length ? util.integerAsBuffer(arg.checksum) : undefined + }; + return this._buildFromBuffers(buffers); +}; + +HDPublicKey.prototype._buildFromSerialized = function (arg) { + var decoded = Base58Check.decode(arg); + var buffers = { + version: decoded.slice(HDPublicKey.VersionStart, HDPublicKey.VersionEnd), + depth: decoded.slice(HDPublicKey.DepthStart, HDPublicKey.DepthEnd), + parentFingerPrint: decoded.slice(HDPublicKey.ParentFingerPrintStart, + HDPublicKey.ParentFingerPrintEnd), + childIndex: decoded.slice(HDPublicKey.ChildIndexStart, HDPublicKey.ChildIndexEnd), + chainCode: decoded.slice(HDPublicKey.ChainCodeStart, HDPublicKey.ChainCodeEnd), + publicKey: decoded.slice(HDPublicKey.PublicKeyStart, HDPublicKey.PublicKeyEnd), + checksum: decoded.slice(HDPublicKey.ChecksumStart, HDPublicKey.ChecksumEnd), + xpubkey: arg + }; + return this._buildFromBuffers(buffers); +}; + +HDPublicKey.prototype._generateRandomly = function (network) { + return HDPublicKey.fromSeed(Random.getRandomBytes(64), network); +}; + +HDPublicKey.fromSeed = function (hexa, network) { + /* jshint maxcomplexity: 8 */ + + if (util.isHexaString(hexa)) { + hexa = util.hexToBuffer(hexa); + } + if (!Buffer.isBuffer(hexa)) { + throw new Error(HDPublicKey.InvalidEntropyArg); + } + if (hexa.length < MINIMUM_ENTROPY_BITS * BITS_TO_BYTES) { + throw new Error(HDPublicKey.NotEnoughEntropy); + } + if (hexa.length > MAXIMUM_ENTROPY_BITS * BITS_TO_BYTES) { + throw new Error('More than 512 bytes of entropy is nonstandard'); + } + var hash = Hash.sha512hmac(hexa, new buffer.Buffer('Bitcoin seed')); + + return new HDPublicKey({ + network: Network.get(network) || Network.livenet, + depth: 0, + parentFingerPrint: 0, + childIndex: 0, + publicKey: hash.slice(0, 32), + chainCode: hash.slice(32, 64) + }); +}; + +/** + * Receives a object with buffers in all the properties and populates the + * internal structure + * + * @param {Object} arg + * @param {buffer.Buffer} arg.version + * @param {buffer.Buffer} arg.depth + * @param {buffer.Buffer} arg.parentFingerPrint + * @param {buffer.Buffer} arg.childIndex + * @param {buffer.Buffer} arg.chainCode + * @param {buffer.Buffer} arg.publicKey + * @param {buffer.Buffer} arg.checksum + * @param {string=} arg.xpubkey - if set, don't recalculate the base58 + * representation + * @return {HDPublicKey} this + */ +HDPublicKey.prototype._buildFromBuffers = function (arg) { + /* jshint maxcomplexity: 8 */ + + HDPublicKey._validateBufferArguments(arg); + this._buffers = arg; + + var sequence = [ + arg.version, arg.depth, arg.parentFingerPrint, arg.childIndex, arg.chainCode, + arg.publicKey + ]; + if (!arg.checksum || !arg.checksum.length) { + arg.checksum = Base58Check.checksum(buffer.Buffer.concat(sequence)); + } else { + if (arg.checksum.toString() !== sequence.toString()) { + throw new Error(HDPublicKey.Errors.InvalidB58Checksum); + } + } + + if (!arg.xpubkey) { + this.xpubkey = Base58Check.encode(buffer.Buffer.concat(sequence)); + } else { + this.xpubkey = arg.xpubkey; + } + + this.network = Network.get(util.integerFromBuffer(arg.version)); + this.depth = util.integerFromSingleByteBuffer(arg.depth); + this.publicKey = PublicKey.fromString(arg.publicKey); + this.fingerPrint = Hash.sha256ripemd160(this.publicKey.toBuffer()).slice(0, HDPublicKey.ParentFingerPrintSize); + + return this; +}; + +HDPublicKey._validateBufferArguments = function (arg) { + var checkBuffer = function(name, size) { + var buff = arg[name]; + assert(buffer.Buffer.isBuffer(buff), name + ' argument is not a buffer, it\'s ' + typeof buff); + assert( + buff.length === size, + name + ' has not the expected size: found ' + buff.length + ', expected ' + size + ); + }; + checkBuffer('version', HDPublicKey.VersionSize); + checkBuffer('depth', HDPublicKey.DepthSize); + checkBuffer('parentFingerPrint', HDPublicKey.ParentFingerPrintSize); + checkBuffer('childIndex', HDPublicKey.ChildIndexSize); + checkBuffer('chainCode', HDPublicKey.ChainCodeSize); + checkBuffer('publicKey', HDPublicKey.PublicKeySize); + if (arg.checksum && arg.checksum.length) { + checkBuffer('checksum', HDPublicKey.CheckSumSize); + } +}; + +HDPublicKey.prototype.toString = function () { + return this.xpubkey; +}; + +HDPublicKey.prototype.toObject = function () { + return { + network: Network.get(util.integerFromBuffer(this._buffers.version)), + depth: util.integerFromSingleByteBuffer(this._buffers.depth), + fingerPrint: util.integerFromBuffer(this.fingerPrint), + parentFingerPrint: util.integerFromBuffer(this._buffers.parentFingerPrint), + childIndex: util.integerFromBuffer(this._buffers.childIndex), + chainCode: util.bufferToHex(this._buffers.chainCode), + publicKey: this.publicKey.toString(), + checksum: util.integerFromBuffer(this._buffers.checksum), + xpubkey: this.xpubkey + }; +}; + +HDPublicKey.prototype.toJson = function () { + return JSON.stringify(this.toObject()); +}; + +HDPublicKey.DefaultDepth = 0; +HDPublicKey.DefaultFingerprint = 0; +HDPublicKey.DefaultChildIndex = 0; +HDPublicKey.DefaultNetwork = Network.livenet; +HDPublicKey.Hardened = 0x80000000; +HDPublicKey.RootElementAlias = ['m', 'M', 'm\'', 'M\'']; + +HDPublicKey.VersionSize = 4; +HDPublicKey.DepthSize = 1; +HDPublicKey.ParentFingerPrintSize = 4; +HDPublicKey.ChildIndexSize = 4; +HDPublicKey.ChainCodeSize = 32; +HDPublicKey.PublicKeySize = 33; +HDPublicKey.CheckSumSize = 4; + +HDPublicKey.SerializedByteSize = 82; + +HDPublicKey.VersionStart = 0; +HDPublicKey.VersionEnd = HDPublicKey.VersionStart + HDPublicKey.VersionSize; +HDPublicKey.DepthStart = HDPublicKey.VersionEnd; +HDPublicKey.DepthEnd = HDPublicKey.DepthStart + HDPublicKey.DepthSize; +HDPublicKey.ParentFingerPrintStart = HDPublicKey.DepthEnd; +HDPublicKey.ParentFingerPrintEnd = HDPublicKey.ParentFingerPrintStart + HDPublicKey.ParentFingerPrintSize; +HDPublicKey.ChildIndexStart = HDPublicKey.ParentFingerPrintEnd; +HDPublicKey.ChildIndexEnd = HDPublicKey.ChildIndexStart + HDPublicKey.ChildIndexSize; +HDPublicKey.ChainCodeStart = HDPublicKey.ChildIndexEnd; +HDPublicKey.ChainCodeEnd = HDPublicKey.ChainCodeStart + HDPublicKey.ChainCodeSize; +HDPublicKey.PublicKeyStart = HDPublicKey.ChainCodeEnd; +HDPublicKey.PublicKeyEnd = HDPublicKey.PublicKeyStart + HDPublicKey.PublicKeySize; +HDPublicKey.ChecksumStart = HDPublicKey.PublicKeyEnd; +HDPublicKey.ChecksumEnd = HDPublicKey.ChecksumStart + HDPublicKey.CheckSumSize; + +assert(HDPublicKey.ChecksumEnd === HDPublicKey.SerializedByteSize); + +HDPublicKey.Errors = {}; +HDPublicKey.Errors.ArgumentIsPrivateExtended = 'Argument starts with xpriv..., it\'s a private key'; +HDPublicKey.Errors.InvalidArgument = 'Invalid argument, expected string or Buffer'; +HDPublicKey.Errors.InvalidB58Char = 'Invalid Base 58 character'; +HDPublicKey.Errors.InvalidB58Checksum = 'Invalid Base 58 checksum'; +HDPublicKey.Errors.InvalidChildIndex = 'Invalid Child Index - must be a number'; +HDPublicKey.Errors.InvalidConstant = 'Unrecognized xpubkey version'; +HDPublicKey.Errors.InvalidDepth = 'Invalid depth parameter - must be a number'; +HDPublicKey.Errors.InvalidDerivationArgument = 'Invalid argument, expected number and boolean or string'; +HDPublicKey.Errors.InvalidEntropyArg = 'Invalid argument: entropy must be an hexa string or binary buffer'; +HDPublicKey.Errors.InvalidLength = 'Invalid length for xpubkey format'; +HDPublicKey.Errors.InvalidNetwork = 'Unexpected version for network'; +HDPublicKey.Errors.InvalidNetworkArgument = 'Network argument must be \'livenet\' or \'testnet\''; +HDPublicKey.Errors.InvalidParentFingerPrint = 'Invalid Parent Fingerprint - must be a number'; +HDPublicKey.Errors.InvalidPath = 'Invalid path for derivation: must start with "m"'; +HDPublicKey.Errors.UnrecognizedArgument = 'Creating a HDPublicKey requires a string, a buffer, a json, or an object'; + +module.exports = HDPublicKey; + diff --git a/lib/networks.js b/lib/networks.js index a57d9e434..24192523c 100644 --- a/lib/networks.js +++ b/lib/networks.js @@ -66,6 +66,7 @@ function getNetwork(arg) { * @namespace Network */ module.exports = { + defaultNetwork: livenet, livenet: livenet, testnet: testnet, mainnet: livenet, diff --git a/lib/privatekey.js b/lib/privatekey.js index 49defee3e..eb9a4dbd8 100644 --- a/lib/privatekey.js +++ b/lib/privatekey.js @@ -8,12 +8,6 @@ var base58check = require('./encoding/base58check'); var Address = require('./address'); var PublicKey = require('./publickey'); -var assert = require('assert'); - -var COMPRESSED_LENGTH = 34; -var UNCOMPRESSED_LENGTH = 33; -var RAW_LENGTH = 32; - /** * * Instantiate a PrivateKey from a BN, Buffer and WIF. @@ -44,10 +38,9 @@ var PrivateKey = function PrivateKey(data, network, compressed) { return new PrivateKey(data, network, compressed); } - network = network || 'livenet'; var info = { compressed: typeof(compressed) !== 'undefined' ? compressed : true, - network: network + network: network || 'mainnet' }; // detect type of data @@ -67,6 +60,9 @@ var PrivateKey = function PrivateKey(data, network, compressed) { if (!info.bn.lt(Point.getN())) { throw new TypeError('Number must be less than N'); } + if (typeof(networks[info.network]) === 'undefined') { + throw new TypeError('Must specify the network ("mainnet" or "testnet")'); + } if (typeof(info.compressed) !== 'boolean') { throw new TypeError('Must specify whether the corresponding public key is compressed or not (true or false)'); } @@ -74,7 +70,6 @@ var PrivateKey = function PrivateKey(data, network, compressed) { this.bn = info.bn; this.compressed = info.compressed; this.network = info.network; - this.publicKey = this.toPublicKey(); return this; @@ -109,29 +104,37 @@ PrivateKey._getRandomBN = function(){ * @private */ PrivateKey._transformBuffer = function(buf, network, compressed) { - /* jshint maxcomplexity: 8 */ var info = {}; - - info.compressed = false; - if (buf.length === COMPRESSED_LENGTH && buf[COMPRESSED_LENGTH-1] === 1) { + if (buf.length === 1 + 32 + 1 && buf[1 + 32 + 1 - 1] === 1) { info.compressed = true; - assert(buf[0] === networks.get(network).privatekey, 'Network version mismatch'); - } else if (buf.length === RAW_LENGTH || buf.length === UNCOMPRESSED_LENGTH) { - if (buf.length === UNCOMPRESSED_LENGTH) { - assert(buf[0] === networks.get(network).privatekey, 'Network version mismatch'); - buf = buf.slice(1, RAW_LENGTH); - } + } else if (buf.length === 1 + 32) { + info.compressed = false; } else { - throw new Error('Length of buffer must be 32 to 34 (plain, uncompressed, or compressed)'); + throw new Error('Length of buffer must be 33 (uncompressed) or 34 (compressed)'); + } + + if (buf[0] === networks.mainnet.privatekey) { + info.network = 'mainnet'; + } else if (buf[0] === networks.testnet.privatekey) { + info.network = 'testnet'; + } else { + throw new Error('Invalid network'); + } + + if (network && networks.get(info.network) !== networks.get(network)) { + throw TypeError('Private key network mismatch'); } if (typeof(compressed) !== 'undefined' && info.compressed !== compressed){ throw TypeError('Private key compression mismatch'); } - info.bn = BN.fromBuffer(buf); + + info.bn = BN.fromBuffer(buf.slice(1, 32 + 1)); + return info; + }; /** diff --git a/lib/publickey.js b/lib/publickey.js index ed00047b6..3bffc6ed1 100644 --- a/lib/publickey.js +++ b/lib/publickey.js @@ -70,7 +70,6 @@ var PublicKey = function PublicKey(data, compressed) { }; /** - * * Internal function to transform a private key into a public key point * * @param {PrivateKey} privkey - An instance of PrivateKey @@ -89,7 +88,6 @@ PublicKey._transformPrivateKey = function(privkey) { }; /** - * * Internal function to transform DER into a public key point * * @param {Buffer} buf - An hex encoded buffer diff --git a/lib/util.js b/lib/util.js index ff8ea8bf5..0651891a7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -44,11 +44,27 @@ module.exports = { integerFromBuffer: function integerFromBuffer(buffer) { return buffer[0] << 24 | buffer[1] << 16 | buffer[2] << 8 | buffer[3]; }, + integerFromSingleByteBuffer: function integerFromBuffer(buffer) { + return buffer[0]; + }, bufferToHex: function bufferToHex(buffer) { return buffer.toString('hex'); }, hexToBuffer: function hexToBuffer(string) { assert(isHexa(string)); return new buffer.Buffer(string, 'hex'); + }, + pointToCompressed: function pointToCompressed(point) { + var xbuf = point.getX().toBuffer({size: 32}); + var ybuf = point.getY().toBuffer({size: 32}); + + var prefix; + var odd = ybuf[ybuf.length - 1] % 2; + if (odd) { + prefix = new Buffer([0x03]); + } else { + prefix = new Buffer([0x02]); + } + return buffer.Buffer.concat([prefix, xbuf]); } }; diff --git a/test/hdkeys.js b/test/hdkeys.js index 10e5cf87c..3e6710d67 100644 --- a/test/hdkeys.js +++ b/test/hdkeys.js @@ -15,7 +15,7 @@ var bitcore = require('..'); var HDPrivateKey = bitcore.HDPrivateKey; var HDPublicKey = bitcore.HDPublicKey; -describe.only('BIP32 compliance', function() { +describe('BIP32 compliance', function() { it('should initialize test vector 1 from the extended public key', function() { new HDPublicKey(vector1_m_public).xpubkey.should.equal(vector1_m_public); @@ -44,8 +44,8 @@ describe.only('BIP32 compliance', function() { }); it("should get m/0' ext. private key from test vector 1", function() { - var privateKey = new HDPrivateKey(vector1_m_private); - privateKey.derive("m/0'").xprivkey.should.equal(vector1_m0h_private); + var privateKey = new HDPrivateKey(vector1_m_private).derive("m/0'"); + privateKey.xprivkey.should.equal(vector1_m0h_private); }); it("should get m/0' ext. public key from test vector 1", function() { @@ -64,8 +64,8 @@ describe.only('BIP32 compliance', function() { }); it("should get m/0'/1 ext. public key from m/0' public key from test vector 1", function() { - var derivedPublic = HDPrivateKey(vector1_m_private).derive("m/0'").hdPublicKey; - derivedPublic.derive("m/1").xpubkey.should.equal(vector1_m0h1_public); + var derivedPublic = HDPrivateKey(vector1_m_private).derive("m/0'").hdPublicKey.derive("m/1"); + derivedPublic.xpubkey.should.equal(vector1_m0h1_public); }); it("should get m/0'/1/2' ext. private key from test vector 1", function() { @@ -130,7 +130,7 @@ describe.only('BIP32 compliance', function() { }); it("should get m/0 ext. public key from m public key from test vector 2", function() { - HDPrivateKey(vector2_m_private).hdPublicKey.derive(0).should.equal(vector2_m0_public); + HDPrivateKey(vector2_m_private).hdPublicKey.derive(0).xpubkey.should.equal(vector2_m0_public); }); it("should get m/0/2147483647h ext. private key from test vector 2", function() {