diff --git a/index.js b/index.js index 092effa84..07e1f7747 100644 --- a/index.js +++ b/index.js @@ -20,9 +20,9 @@ bitcore.encoding.Varint = require('./lib/encoding/varint'); // main bitcoin library bitcore.Address = require('./lib/address'); -bitcore.BIP32 = require('./lib/bip32'); bitcore.Block = require('./lib/block'); bitcore.Blockheader = require('./lib/blockheader'); +bitcore.HDPrivateKey = require('./lib/hdprivkey.js'); bitcore.Networks = require('./lib/networks'); bitcore.Opcode = require('./lib/opcode'); bitcore.PrivateKey = require('./lib/privatekey'); diff --git a/lib/encoding/base58.js b/lib/encoding/base58.js index 6878b73fb..6b869ec55 100644 --- a/lib/encoding/base58.js +++ b/lib/encoding/base58.js @@ -1,10 +1,16 @@ 'use strict'; +var _ = require('lodash'); var bs58 = require('bs58'); +var buffer = require('buffer'); + +var ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'.split(''); var Base58 = function Base58(obj) { - if (!(this instanceof Base58)) + /* jshint maxcomplexity: 8 */ + if (!(this instanceof Base58)) { return new Base58(obj); + } if (Buffer.isBuffer(obj)) { var buf = obj; this.fromBuffer(buf); @@ -16,20 +22,29 @@ var Base58 = function Base58(obj) { } }; +Base58.validCharacters = function validCharacters(chars) { + if (buffer.Buffer.isBuffer(chars)) { + chars = chars.toString(); + } + return _.all(_.map(chars, function(char) { return _.contains(ALPHABET, char); })); +}; + Base58.prototype.set = function(obj) { this.buf = obj.buf || this.buf || undefined; return this; }; Base58.encode = function(buf) { - if (!Buffer.isBuffer(buf)) + if (!buffer.Buffer.isBuffer(buf)) { throw new Error('Input should be a buffer'); + } return bs58.encode(buf); }; Base58.decode = function(str) { - if (typeof str !== 'string') + if (typeof str !== 'string') { throw new Error('Input should be a string'); + } return new Buffer(bs58.decode(str)); }; diff --git a/lib/encoding/base58check.js b/lib/encoding/base58check.js index ea4ca316f..e7b7b4342 100644 --- a/lib/encoding/base58check.js +++ b/lib/encoding/base58check.js @@ -1,6 +1,8 @@ 'use strict'; +var _ = require('lodash'); var base58 = require('./base58'); +var buffer = require('buffer'); var sha256sha256 = require('../crypto/hash').sha256sha256; var Base58Check = function Base58Check(obj) { @@ -22,6 +24,20 @@ Base58Check.prototype.set = function(obj) { return this; }; +Base58Check.validChecksum = function validChecksum(data, checksum) { + if (_.isString(data)) { + data = new buffer.Buffer(base58.decode(data)); + } + if (_.isString(checksum)) { + checksum = new buffer.Buffer(base58.decode(checksum)); + } + if (!checksum) { + checksum = data.slice(-4); + data = data.slice(0, -4); + } + return Base58Check.checksum(data).toString('hex') === checksum.toString('hex'); +}; + Base58Check.decode = function(s) { if (typeof s !== 'string') throw new Error('Input must be a string'); diff --git a/lib/hdprivkey.js b/lib/hdprivkey.js index 4f83d5728..0006a618f 100644 --- a/lib/hdprivkey.js +++ b/lib/hdprivkey.js @@ -5,9 +5,9 @@ var BN = require('./crypto/bn'); var Base58 = require('./encoding/base58'); var Base58Check = require('./encoding/base58check'); var Hash = require('./crypto/hash'); -var Network = require('./network'); +var Network = require('./networks'); var Point = require('./crypto/point'); -var PrivateKey = require('./privkey'); +var PrivateKey = require('./privatekey'); var Random = require('./crypto/random'); var assert = require('assert'); @@ -15,7 +15,7 @@ var buffer = require('buffer'); var util = require('./util'); var MINIMUM_ENTROPY_BITS = 128; -var BITS_TO_BYTES = 128; +var BITS_TO_BYTES = 1/8; var MAXIMUM_ENTROPY_BITS = 512; @@ -24,21 +24,21 @@ function HDPrivateKey(arg) { if (arg instanceof HDPrivateKey) { return arg; } - if (!this instanceof HDPrivateKey) { + if (!(this instanceof HDPrivateKey)) { return new HDPrivateKey(arg); } if (arg) { if (_.isString(arg) || buffer.Buffer.isBuffer(arg)) { if (HDPrivateKey.isValidSerialized(arg)) { this._buildFromSerialized(arg); - } else if (util.isValidJson(arg)) { - this._buildFromJson(arg); } else { - throw new Error(HDPrivateKey.Errors.UnrecognizedArgument); + throw new Error(HDPrivateKey.getSerializedError(arg)); } } else { if (_.isObject(arg)) { this._buildFromObject(arg); + } else if (util.isValidJson(arg)) { + this._buildFromJson(arg); } else { throw new Error(HDPrivateKey.Errors.UnrecognizedArgument); } @@ -70,11 +70,12 @@ HDPrivateKey.prototype._deriveWithNumber = function deriveWithNumber(index, hard } else { data = buffer.Buffer.concat([this.publicKey.toBuffer(), indexBuffer]); } - var hash = Hash.sha512hmac(data, this.chainCode); + 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 privateKey = leftPart.add(this.privateKey.toBigNumber()).mod(Point.getN()); + var privateKey = leftPart.add(this.privateKey.toBigNumber()).mod(Point.getN()).toBuffer({size: 32}); + console.log(privateKey); return new HDPrivateKey({ network: this.network, @@ -134,13 +135,12 @@ HDPrivateKey.getSerializedError = function getSerializedError(data, network) { if (!(_.isString(data) || buffer.Buffer.isBuffer(data))) { return HDPrivateKey.Errors.InvalidArgument; } - if (_.isString(data)) { - data = new buffer.Buffer(data); - } if (!Base58.validCharacters(data)) { return HDPrivateKey.Errors.InvalidB58Char; } - if (!Base58Check.validChecksum(data)) { + try { + data = Base58Check.decode(data); + } catch (e) { return HDPrivateKey.Errors.InvalidB58Checksum; } if (data.length !== 78) { @@ -175,12 +175,12 @@ HDPrivateKey.prototype._buildFromObject = function buildFromObject(arg) { // TODO: Type validation var buffers = { version: util.integerAsBuffer(Network.get(arg.network).xprivkey), - depth: util.integerAsBuffer(arg.depth), + depth: util.integerAsSingleByteBuffer(arg.depth), parentFingerPrint: util.integerAsBuffer(arg.parentFingerPrint), childIndex: util.integerAsBuffer(arg.childIndex), - chainCode: util.integerAsBuffer(arg.chainCode), - privateKey: util.hexToBuffer(arg.privateKey), - checksum: util.integerAsBuffer(arg.checksum) + 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 }; return this._buildFromBuffers(buffers); }; @@ -196,7 +196,7 @@ HDPrivateKey.prototype._buildFromSerialized = function buildFromSerialized(arg) chainCode: decoded.slice(HDPrivateKey.ChainCodeStart, HDPrivateKey.ChainCodeEnd), privateKey: decoded.slice(HDPrivateKey.PrivateKeyStart, HDPrivateKey.PrivateKeyEnd), checksum: decoded.slice(HDPrivateKey.ChecksumStart, HDPrivateKey.ChecksumEnd), - xprivkey: decoded.toString() + xprivkey: arg }; return this._buildFromBuffers(buffers); }; @@ -227,8 +227,8 @@ HDPrivateKey.fromSeed = function fromSeed(hexa, network) { depth: 0, parentFingerPrint: 0, childIndex: 0, - chainCode: hash.slice(32, 64), - privateKey: hash.slice(0, 32) + privateKey: hash.slice(0, 32), + chainCode: hash.slice(32, 64) }); }; @@ -249,16 +249,21 @@ HDPrivateKey.fromSeed = function fromSeed(hexa, network) { * @return {HDPrivateKey} this */ HDPrivateKey.prototype._buildFromBuffers = function buildFromBuffers(arg) { + /* jshint maxcomplexity: 8 */ + 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, + util.emptyBuffer(1), arg.privateKey ]; - if (!arg.checksum) { - arg.checksum = Base58Check.checksum(sequence); + console.log(arg.privateKey); + console.log(sequence); + if (!arg.checksum || !arg.checksum.length) { + arg.checksum = Base58Check.checksum(buffer.Buffer.concat(sequence)); } else { if (arg.checksum.toString() !== sequence.toString()) { throw new Error(HDPrivateKey.Errors.InvalidB58Checksum); @@ -275,29 +280,32 @@ HDPrivateKey.prototype._buildFromBuffers = function buildFromBuffers(arg) { // 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(this.publicKey._value); + this.fingerPrint = Base58Check.checksum(util.hexToBuffer(this.publicKey.toString())); return this; }; HDPrivateKey._validateBufferArguments = function validateBufferArguments(arg) { var checkBuffer = function(name, size) { - var buffer = arg[name]; - assert(buffer.Buffer.isBuffer(buffer), name + ' argument is not a buffer'); + var buff = arg[name]; + assert(buffer.Buffer.isBuffer(buff), name + ' argument is not a buffer'); assert( - buffer.length === size, - name + ' has not the expected size: found ' + buffer.length + ', expected ' + size + buff.length === size, + name + ' has not the expected size: found ' + buff.length + ', expected ' + size ); }; checkBuffer('version', HDPrivateKey.VersionSize); - checkBuffer('depth', HDPrivateKey.DepthLength); + checkBuffer('depth', HDPrivateKey.DepthSize); checkBuffer('parentFingerPrint', HDPrivateKey.ParentFingerPrintSize); checkBuffer('childIndex', HDPrivateKey.ChildIndexSize); checkBuffer('chainCode', HDPrivateKey.ChainCodeSize); checkBuffer('privateKey', HDPrivateKey.PrivateKeySize); - checkBuffer('checksum', HDPrivateKey.CheckSumSize); + if (arg.checksum && arg.checksum.length) { + checkBuffer('checksum', HDPrivateKey.CheckSumSize); + } }; HDPrivateKey.prototype.toString = function toString() { @@ -330,22 +338,31 @@ HDPrivateKey.Hardened = 0x80000000; HDPrivateKey.RootElementAlias = ['m', 'M', 'm\'', 'M\'']; HDPrivateKey.VersionSize = 4; -HDPrivateKey.DepthLength = 4; +HDPrivateKey.DepthSize = 1; HDPrivateKey.ParentFingerPrintSize = 4; HDPrivateKey.ChildIndexSize = 4; HDPrivateKey.ChainCodeSize = 32; HDPrivateKey.PrivateKeySize = 32; HDPrivateKey.CheckSumSize = 4; -HDPrivateKey.VersionStart = 0; -HDPrivateKey.VersionEnd = HDPrivateKey.DepthStart = 4; -HDPrivateKey.DepthEnd = HDPrivateKey.ParentFingerPrintStart = 8; -HDPrivateKey.ParentFingerPrintEnd = HDPrivateKey.ChildIndexStart = 12; -HDPrivateKey.ChildIndexEnd = HDPrivateKey.ChainCodeStart = 16; -HDPrivateKey.ChainCodeEnd = 32; -HDPrivateKey.PrivateKeyStart = 33; -HDPrivateKey.PrivateKeyEnd = HDPrivateKey.ChecksumStart = 65; -HDPrivateKey.ChecksumEnd = 69; +HDPrivateKey.SerializedByteSize = 82; + +HDPrivateKey.VersionStart = 0; +HDPrivateKey.VersionEnd = HDPrivateKey.VersionStart + HDPrivateKey.VersionSize; +HDPrivateKey.DepthStart = HDPrivateKey.VersionEnd; +HDPrivateKey.DepthEnd = HDPrivateKey.DepthStart + HDPrivateKey.DepthSize; +HDPrivateKey.ParentFingerPrintStart = HDPrivateKey.DepthEnd; +HDPrivateKey.ParentFingerPrintEnd = HDPrivateKey.ParentFingerPrintStart + HDPrivateKey.ParentFingerPrintSize; +HDPrivateKey.ChildIndexStart = HDPrivateKey.ParentFingerPrintEnd; +HDPrivateKey.ChildIndexEnd = HDPrivateKey.ChildIndexStart + HDPrivateKey.ChildIndexSize; +HDPrivateKey.ChainCodeStart = HDPrivateKey.ChildIndexEnd; +HDPrivateKey.ChainCodeEnd = HDPrivateKey.ChainCodeStart + HDPrivateKey.ChainCodeSize; +HDPrivateKey.PrivateKeyStart = HDPrivateKey.ChainCodeEnd + 1; +HDPrivateKey.PrivateKeyEnd = HDPrivateKey.PrivateKeyStart + HDPrivateKey.PrivateKeySize; +HDPrivateKey.ChecksumStart = HDPrivateKey.PrivateKeyEnd; +HDPrivateKey.ChecksumEnd = HDPrivateKey.ChecksumStart + HDPrivateKey.CheckSumSize; + +assert(HDPrivateKey.ChecksumEnd === HDPrivateKey.SerializedByteSize); HDPrivateKey.Errors = {}; HDPrivateKey.Errors.InvalidArgument = 'Invalid argument, expected string or Buffer'; diff --git a/lib/privatekey.js b/lib/privatekey.js index 0d12ce395..49defee3e 100644 --- a/lib/privatekey.js +++ b/lib/privatekey.js @@ -8,6 +8,12 @@ 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. @@ -38,9 +44,10 @@ 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 || 'mainnet' + network: network }; // detect type of data @@ -60,9 +67,6 @@ 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)'); } @@ -70,6 +74,7 @@ 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; @@ -104,37 +109,29 @@ PrivateKey._getRandomBN = function(){ * @private */ PrivateKey._transformBuffer = function(buf, network, compressed) { + /* jshint maxcomplexity: 8 */ var info = {}; - if (buf.length === 1 + 32 + 1 && buf[1 + 32 + 1 - 1] === 1) { + + info.compressed = false; + if (buf.length === COMPRESSED_LENGTH && buf[COMPRESSED_LENGTH-1] === 1) { info.compressed = true; - } else if (buf.length === 1 + 32) { - info.compressed = false; + 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 { - 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 && info.network !== network){ - throw TypeError('Private key network mismatch'); + throw new Error('Length of buffer must be 32 to 34 (plain, uncompressed, or compressed)'); } if (typeof(compressed) !== 'undefined' && info.compressed !== compressed){ throw TypeError('Private key compression mismatch'); } - - info.bn = BN.fromBuffer(buf.slice(1, 32 + 1)); - + info.bn = BN.fromBuffer(buf); return info; - }; /** diff --git a/lib/util.js b/lib/util.js index 05d476739..ff8ea8bf5 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,6 +1,15 @@ 'use strict'; var _ = require('lodash'); +var buffer = require('buffer'); +var assert = require('assert'); + +var isHexa = function isHexa(value) { + if (!_.isString(value)) { + return false; + } + return /^[0-9a-fA-F]+$/.test(value); +}; module.exports = { isValidJson: function isValidJson(arg) { @@ -16,6 +25,10 @@ module.exports = { for (var i = 0; i < bytes; i++) { result.write('\0', i); } + return result; + }, + integerAsSingleByteBuffer: function integerAsSingleByteBuffer(integer) { + return new Buffer([integer & 0xff]); }, integerAsBuffer: function integerAsBuffer(integer) { var bytes = []; @@ -25,16 +38,17 @@ module.exports = { bytes.push(integer & 0xff); return new Buffer(bytes); }, - isHexa: function isHexa(value) { - if (!_.isString(value)) { - return false; - } - return /^[0-9a-fA-F]+$/.test(value); - }, + isHexa: isHexa, + isHexaString: isHexa, + integerFromBuffer: function integerFromBuffer(buffer) { return buffer[0] << 24 | buffer[1] << 16 | buffer[2] << 8 | buffer[3]; }, bufferToHex: function bufferToHex(buffer) { return buffer.toString('hex'); + }, + hexToBuffer: function hexToBuffer(string) { + assert(isHexa(string)); + return new buffer.Buffer(string, 'hex'); } }; diff --git a/test/encoding/base58.js b/test/encoding/base58.js index 3604e9c2a..fc0a7e858 100644 --- a/test/encoding/base58.js +++ b/test/encoding/base58.js @@ -2,10 +2,11 @@ var should = require('chai').should(); var bitcore = require('../..'); +var buffer = require('buffer'); var Base58 = bitcore.encoding.Base58; describe('Base58', function() { - var buf = new Buffer([0, 1, 2, 3, 253, 254, 255]); + var buf = new buffer.Buffer([0, 1, 2, 3, 253, 254, 255]); var enc = '1W7N4RuG'; it('should make an instance with "new"', function() { @@ -13,6 +14,21 @@ describe('Base58', function() { should.exist(b58); }); + it('validates characters with no false negatives', function() { + Base58.validCharacters( + '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + ).should.equal(true); + }); + it('validates characters from buffer', function() { + Base58.validCharacters( + new buffer.Buffer('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz') + ).should.equal(true); + }); + + it('some characters are invalid (no false positives)', function() { + Base58.validCharacters('!@#%^$&*()\\').should.equal(false); + }); + it('should make an instance without "new"', function() { var b58 = Base58(); should.exist(b58); @@ -27,7 +43,7 @@ describe('Base58', function() { it('should set a blank buffer', function() { Base58().set({ - buf: new Buffer([]) + buf: new buffer.Buffer([]) }); }); diff --git a/test/encoding/base58check.js b/test/encoding/base58check.js index 0082b5ef5..b8b56c888 100644 --- a/test/encoding/base58check.js +++ b/test/encoding/base58check.js @@ -14,6 +14,13 @@ describe('Base58Check', function() { should.exist(b58); }); + it('can validate a serialized string', function() { + var address = '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy'; + Base58Check.validChecksum(address).should.equal(true); + address = address + 'a'; + Base58Check.validChecksum(address).should.equal(false); + }); + it('should make an instance without "new"', function() { var b58 = Base58Check(); should.exist(b58); diff --git a/test/hdkeys.js b/test/hdkeys.js index ad19aafaa..10e5cf87c 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('BIP32 compliance', function() { +describe.only('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);