UNPKG

16.1 kBJavaScriptView Raw
1'use strict';
2
3var owsCommon = require('@owstack/ows-common');
4var assert = require('assert');
5var Base58 = owsCommon.encoding.Base58;
6var Base58Check = owsCommon.encoding.Base58Check;
7var BN = owsCommon.BN;
8var BufferUtil = owsCommon.buffer;
9var errors = owsCommon.errors;
10var Hash = owsCommon.Hash;
11var JSUtil = owsCommon.util.js;
12var hdErrors = errors.HDPublicKey;
13var HDPrivateKey = require('./hdprivatekey');
14var Networks = require('@owstack/network-lib');
15var Point = require('./crypto/point');
16var PublicKey = require('./publickey');
17var lodash = owsCommon.deps.lodash;
18var $ = owsCommon.util.preconditions;
19
20/**
21 * The representation of an hierarchically derived public key.
22 *
23 * See https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
24 *
25 * @constructor
26 * @param {Object|string|Buffer} arg
27 */
28//function HDPublicKey(arg, network) {
29function HDPublicKey(arg) {
30 /* jshint maxcomplexity: 12 */
31 /* jshint maxstatements: 20 */
32 if (arg instanceof HDPublicKey) {
33 return arg;
34 }
35 if (!(this instanceof HDPublicKey)) {
36 return new HDPublicKey(arg);
37 }
38 if (arg) {
39 if (lodash.isString(arg) || BufferUtil.isBuffer(arg)) {
40 var error = HDPublicKey.getSerializedError(arg);
41 if (!error) {
42 return this._buildFromSerialized(arg);
43 } else if (BufferUtil.isBuffer(arg) && !HDPublicKey.getSerializedError(arg.toString())) {
44 return this._buildFromSerialized(arg.toString());
45 } else {
46 if (error instanceof hdErrors.ArgumentIsPrivateExtended) {
47 return new HDPrivateKey(arg).hdPublicKey;
48 }
49 throw error;
50 }
51 } else {
52 if (lodash.isObject(arg)) {
53 if (arg instanceof HDPrivateKey) {
54 return this._buildFromPrivate(arg);
55 } else {
56 return this._buildFromObject(arg);
57 }
58 } else {
59 throw new hdErrors.UnrecognizedArgument(arg);
60 }
61 }
62 } else {
63 throw new hdErrors.MustSupplyArgument();
64 }
65}
66
67/**
68 * Verifies that a given path is valid.
69 *
70 * @param {string|number} arg
71 * @return {boolean}
72 */
73HDPublicKey.isValidPath = function(arg) {
74 if (lodash.isString(arg)) {
75 var indexes = HDPrivateKey._getDerivationIndexes(arg);
76 return indexes !== null && lodash.every(indexes, HDPublicKey.isValidPath);
77 }
78
79 if (lodash.isNumber(arg)) {
80 return arg >= 0 && arg < HDPublicKey.Hardened;
81 }
82
83 return false;
84};
85
86/**
87 * Get a derivated child based on a string or number.
88 *
89 * If the first argument is a string, it's parsed as the full path of
90 * derivation. Valid values for this argument include "m" (which returns the
91 * same public key), "m/0/1/40/2/1000".
92 *
93 * Note that hardened keys can't be derived from a public extended key.
94 *
95 * If the first argument is a number, the child with that index will be
96 * derived. See the example usage for clarification.
97 *
98 * @example
99 * ```javascript
100 * var parent = new HDPublicKey('xpub...');
101 * var child_0_1_2 = parent.deriveChild(0).deriveChild(1).deriveChild(2);
102 * var copy_of_child_0_1_2 = parent.deriveChild("m/0/1/2");
103 * assert(child_0_1_2.xprivkey === copy_of_child_0_1_2);
104 * ```
105 *
106 * @param {string|number} arg
107 */
108HDPublicKey.prototype.deriveChild = function(arg, hardened) {
109 if (lodash.isNumber(arg)) {
110 return this._deriveWithNumber(arg, hardened);
111 } else if (lodash.isString(arg)) {
112 return this._deriveFromString(arg);
113 } else {
114 throw new hdErrors.InvalidDerivationArgument(arg);
115 }
116};
117
118HDPublicKey.prototype._deriveWithNumber = function(index, hardened) {
119 if (index >= HDPublicKey.Hardened || hardened) {
120 throw new hdErrors.InvalidIndexCantDeriveHardened();
121 }
122 if (index < 0) {
123 throw new hdErrors.InvalidPath(index);
124 }
125
126 var indexBuffer = BufferUtil.integerAsBuffer(index);
127 var data = BufferUtil.concat([this.publicKey.toBuffer(), indexBuffer]);
128 var hash = Hash.sha512hmac(data, this._buffers.chainCode);
129 var leftPart = BN.fromBuffer(hash.slice(0, 32), {size: 32});
130 var chainCode = hash.slice(32, 64);
131
132 var publicKey;
133 try {
134 publicKey = PublicKey.fromPoint(Point.getG().mul(leftPart).add(this.publicKey.point));
135 } catch (e) {
136 return this._deriveWithNumber(index + 1);
137 }
138
139 var derived = new HDPublicKey({
140 network: this.network || Networks.defaultNetwork,
141 depth: this.depth + 1,
142 parentFingerPrint: this.fingerPrint,
143 childIndex: index,
144 chainCode: chainCode,
145 publicKey: publicKey
146 });
147
148 return derived;
149};
150
151HDPublicKey.prototype._deriveFromString = function(path) {
152 /* jshint maxcomplexity: 8 */
153 if (lodash.includes(path, "'")) {
154 throw new hdErrors.InvalidIndexCantDeriveHardened();
155 } else if (!HDPublicKey.isValidPath(path)) {
156 throw new hdErrors.InvalidPath(path);
157 }
158
159 var indexes = HDPrivateKey._getDerivationIndexes(path);
160 var derived = indexes.reduce(function(prev, index) {
161 return prev._deriveWithNumber(index);
162 }, this);
163
164 return derived;
165};
166
167/**
168 * Verifies that a given serialized public key in base58 with checksum format
169 * is valid.
170 *
171 * @param {string|Buffer} data - the serialized public key
172 * @param {string|Network=} network - optional, if present, checks that the network provided matches the network serialized.
173 * @return {boolean}
174 */
175HDPublicKey.isValidSerialized = function(data, network) {
176 return lodash.isNull(HDPublicKey.getSerializedError(data, network));
177};
178
179/**
180 * Checks what's the error that causes the validation of a serialized public key
181 * in base58 with checksum to fail.
182 *
183 * @param {string|Buffer} data - the serialized public key
184 * @param {string|Network=} network - optional, if present, checks that the network provided matches the network serialized.
185 * @return {errors|null}
186 */
187HDPublicKey.getSerializedError = function(data, network) {
188 /* jshint maxcomplexity: 10 */
189 /* jshint maxstatements: 20 */
190 if (!(lodash.isString(data) || BufferUtil.isBuffer(data))) {
191 return new hdErrors.UnrecognizedArgument('expected buffer or string');
192 }
193 if (!Base58.validCharacters(data)) {
194 return new errors.InvalidB58Char('(unknown)', data);
195 }
196 try {
197 data = Base58Check.decode(data);
198 } catch (e) {
199 return new errors.InvalidB58Checksum(data);
200 }
201 if (data.length !== HDPublicKey.DataSize) {
202 return new hdErrors.InvalidLength(data);
203 }
204
205 var version = BufferUtil.integerFromBuffer(data.slice(0, 4));
206
207 if (!lodash.isUndefined(network)) {
208 var error = HDPublicKey._validateNetwork(data, network);
209 if (error) {
210 return error;
211 }
212 }
213
214 if (version === lodash.get(Networks.get(version), 'version.xprivkey.bytes')) {
215 return new hdErrors.ArgumentIsPrivateExtended();
216 }
217 return null;
218};
219
220HDPublicKey._validateNetwork = function(data, networkArg) {
221 var network = Networks.get(networkArg);
222 if (!network) {
223 return new errors.InvalidNetworkArgument(networkArg);
224 }
225 var version = data.slice(HDPublicKey.VersionStart, HDPublicKey.VersionEnd);
226
227 if (!(BufferUtil.integerFromBuffer(version) == network.version.xpubkey.bytes || BufferUtil.integerFromBuffer(version) == network.version.xprivkey.bytes)) {
228 return new errors.InvalidNetwork(version);
229 }
230 return null;
231};
232
233HDPublicKey.prototype._buildFromPrivate = function (arg) {
234 var args = lodash.clone(arg._buffers);
235 var point = Point.getG().mul(BN.fromBuffer(args.privateKey));
236 args.publicKey = Point.pointToCompressed(point);
237 args.version = BufferUtil.integerAsBuffer(Networks.get(BufferUtil.integerFromBuffer(args.version)).version.xpubkey.bytes);
238 args.privateKey = undefined;
239 args.checksum = undefined;
240 args.xprivkey = undefined;
241 return this._buildFromBuffers(args);
242};
243
244HDPublicKey.prototype._buildFromObject = function(arg) {
245 /* jshint maxcomplexity: 10 */
246 // TODO: Type validation
247 var buffers = {
248 version: arg.network ? BufferUtil.integerAsBuffer(Networks.get(arg.network).version.xpubkey.bytes) : arg.version,
249 depth: lodash.isNumber(arg.depth) ? BufferUtil.integerAsSingleByteBuffer(arg.depth) : arg.depth,
250 parentFingerPrint: lodash.isNumber(arg.parentFingerPrint) ? BufferUtil.integerAsBuffer(arg.parentFingerPrint) : arg.parentFingerPrint,
251 childIndex: lodash.isNumber(arg.childIndex) ? BufferUtil.integerAsBuffer(arg.childIndex) : arg.childIndex,
252 chainCode: lodash.isString(arg.chainCode) ? BufferUtil.hexToBuffer(arg.chainCode) : arg.chainCode,
253 publicKey: lodash.isString(arg.publicKey) ? BufferUtil.hexToBuffer(arg.publicKey) :
254 BufferUtil.isBuffer(arg.publicKey) ? arg.publicKey : arg.publicKey.toBuffer(),
255 checksum: lodash.isNumber(arg.checksum) ? BufferUtil.integerAsBuffer(arg.checksum) : arg.checksum
256 };
257 return this._buildFromBuffers(buffers);
258};
259
260HDPublicKey.prototype._buildFromSerialized = function(arg) {
261 var decoded = Base58Check.decode(arg);
262 var buffers = {
263 version: decoded.slice(HDPublicKey.VersionStart, HDPublicKey.VersionEnd),
264 depth: decoded.slice(HDPublicKey.DepthStart, HDPublicKey.DepthEnd),
265 parentFingerPrint: decoded.slice(HDPublicKey.ParentFingerPrintStart, HDPublicKey.ParentFingerPrintEnd),
266 childIndex: decoded.slice(HDPublicKey.ChildIndexStart, HDPublicKey.ChildIndexEnd),
267 chainCode: decoded.slice(HDPublicKey.ChainCodeStart, HDPublicKey.ChainCodeEnd),
268 publicKey: decoded.slice(HDPublicKey.PublicKeyStart, HDPublicKey.PublicKeyEnd),
269 checksum: decoded.slice(HDPublicKey.ChecksumStart, HDPublicKey.ChecksumEnd),
270 xpubkey: arg
271 };
272 return this._buildFromBuffers(buffers);
273};
274
275/**
276 * Receives a object with buffers in all the properties and populates the
277 * internal structure
278 *
279 * @param {Object} arg
280 * @param {buffer.Buffer} arg.version
281 * @param {buffer.Buffer} arg.depth
282 * @param {buffer.Buffer} arg.parentFingerPrint
283 * @param {buffer.Buffer} arg.childIndex
284 * @param {buffer.Buffer} arg.chainCode
285 * @param {buffer.Buffer} arg.publicKey
286 * @param {buffer.Buffer} arg.checksum
287 * @param {string=} arg.xpubkey - if set, don't recalculate the base58
288 * representation
289 * @return {HDPublicKey} this
290 */
291HDPublicKey.prototype._buildFromBuffers = function(arg) {
292 /* jshint maxcomplexity: 8 */
293 /* jshint maxstatements: 20 */
294 HDPublicKey._validateBufferArguments(arg);
295
296 JSUtil.defineImmutable(this, {
297 _buffers: arg
298 });
299
300 var sequence = [
301 arg.version, arg.depth, arg.parentFingerPrint, arg.childIndex, arg.chainCode, arg.publicKey
302 ];
303
304 var concat = BufferUtil.concat(sequence);
305 var checksum = Base58Check.checksum(concat);
306 if (!arg.checksum || !arg.checksum.length) {
307 arg.checksum = checksum;
308 } else {
309 if (arg.checksum.toString('hex') !== checksum.toString('hex')) {
310 throw new errors.InvalidB58Checksum(concat, checksum);
311 }
312 }
313 var network = Networks.get(BufferUtil.integerFromBuffer(arg.version));
314
315 var xpubkey;
316 xpubkey = Base58Check.encode(BufferUtil.concat(sequence));
317 arg.xpubkey = new Buffer(xpubkey);
318 var publicKey = new PublicKey(arg.publicKey, network);
319 var size = HDPublicKey.ParentFingerPrintSize;
320 var fingerPrint = Hash.sha256ripemd160(publicKey.toBuffer()).slice(0, size);
321
322 JSUtil.defineImmutable(this, {
323 xpubkey: xpubkey,
324 network: network,
325 depth: BufferUtil.integerFromSingleByteBuffer(arg.depth),
326 publicKey: publicKey,
327 fingerPrint: fingerPrint
328 });
329
330 return this;
331};
332
333HDPublicKey._validateBufferArguments = function(arg) {
334 var checkBuffer = function(name, size) {
335 var buff = arg[name];
336 assert(BufferUtil.isBuffer(buff), name + ' argument is not a buffer, it\'s ' + typeof buff);
337 assert(
338 buff.length === size,
339 name + ' has not the expected size: found ' + buff.length + ', expected ' + size
340 );
341 };
342 checkBuffer('version', HDPublicKey.VersionSize);
343 checkBuffer('depth', HDPublicKey.DepthSize);
344 checkBuffer('parentFingerPrint', HDPublicKey.ParentFingerPrintSize);
345 checkBuffer('childIndex', HDPublicKey.ChildIndexSize);
346 checkBuffer('chainCode', HDPublicKey.ChainCodeSize);
347 checkBuffer('publicKey', HDPublicKey.PublicKeySize);
348 if (arg.checksum && arg.checksum.length) {
349 checkBuffer('checksum', HDPublicKey.CheckSumSize);
350 }
351};
352
353HDPublicKey.fromString = function(arg) {
354 $.checkArgument(lodash.isString(arg), 'No valid string was provided');
355 return new HDPublicKey(arg);
356};
357
358HDPublicKey.fromObject = function(arg) {
359 $.checkArgument(lodash.isObject(arg), 'No valid argument was provided');
360 return new HDPublicKey(arg);
361};
362
363/**
364 * Returns the base58 checked representation of the public key
365 * @return {string} a string starting with "xpub..." for BTC
366 */
367HDPublicKey.prototype.toString = function() {
368 return this.xpubkey;
369};
370
371/**
372 * Returns the console representation of this extended public key.
373 * @return string
374 */
375HDPublicKey.prototype.inspect = function() {
376 return '<HDPublicKey: ' + this.xpubkey + '>';
377};
378
379/**
380 * Returns a plain JavaScript object with information to reconstruct a key.
381 *
382 * Fields are: <ul>
383 * <li> network: Network name
384 * <li> depth: a number from 0 to 255, the depth to the master extended key
385 * <li> fingerPrint: a number of 32 bits taken from the hash of the public key
386 * <li> fingerPrint: a number of 32 bits taken from the hash of this key's parent's public key
387 * <li> childIndex: index with which this key was derived
388 * <li> chainCode: string in hexa encoding used for derivation
389 * <li> publicKey: string, hexa encoded, in compressed key format
390 * <li> checksum: BufferUtil.integerFromBuffer(this._buffers.checksum),
391 * <li> xpubkey: the string with the base58 representation of this extended key
392 * <li> checksum: the base58 checksum of xpubkey
393 * </ul>
394 */
395HDPublicKey.prototype.toObject = HDPublicKey.prototype.toJSON = function toObject() {
396 return {
397 network: Networks.get(BufferUtil.integerFromBuffer(this._buffers.version)).name,
398 depth: BufferUtil.integerFromSingleByteBuffer(this._buffers.depth),
399 fingerPrint: BufferUtil.integerFromBuffer(this.fingerPrint),
400 parentFingerPrint: BufferUtil.integerFromBuffer(this._buffers.parentFingerPrint),
401 childIndex: BufferUtil.integerFromBuffer(this._buffers.childIndex),
402 chainCode: BufferUtil.bufferToHex(this._buffers.chainCode),
403 publicKey: this.publicKey.toString(),
404 checksum: BufferUtil.integerFromBuffer(this._buffers.checksum),
405 xpubkey: this.xpubkey
406 };
407};
408
409/**
410 * Create a HDPublicKey from a buffer argument
411 *
412 * @param {Buffer} arg
413 * @return {HDPublicKey}
414 */
415HDPublicKey.fromBuffer = function(arg) {
416 return new HDPublicKey(arg);
417};
418
419/**
420 * Return a buffer representation of the xpubkey
421 *
422 * @return {Buffer}
423 */
424HDPublicKey.prototype.toBuffer = function() {
425 return BufferUtil.copy(this._buffers.xpubkey);
426};
427
428HDPublicKey.Hardened = 0x80000000;
429HDPublicKey.RootElementAlias = ['m', 'M'];
430
431HDPublicKey.VersionSize = 4;
432HDPublicKey.DepthSize = 1;
433HDPublicKey.ParentFingerPrintSize = 4;
434HDPublicKey.ChildIndexSize = 4;
435HDPublicKey.ChainCodeSize = 32;
436HDPublicKey.PublicKeySize = 33;
437HDPublicKey.CheckSumSize = 4;
438
439HDPublicKey.DataSize = 78;
440HDPublicKey.SerializedByteSize = 82;
441
442HDPublicKey.VersionStart = 0;
443HDPublicKey.VersionEnd = HDPublicKey.VersionStart + HDPublicKey.VersionSize;
444HDPublicKey.DepthStart = HDPublicKey.VersionEnd;
445HDPublicKey.DepthEnd = HDPublicKey.DepthStart + HDPublicKey.DepthSize;
446HDPublicKey.ParentFingerPrintStart = HDPublicKey.DepthEnd;
447HDPublicKey.ParentFingerPrintEnd = HDPublicKey.ParentFingerPrintStart + HDPublicKey.ParentFingerPrintSize;
448HDPublicKey.ChildIndexStart = HDPublicKey.ParentFingerPrintEnd;
449HDPublicKey.ChildIndexEnd = HDPublicKey.ChildIndexStart + HDPublicKey.ChildIndexSize;
450HDPublicKey.ChainCodeStart = HDPublicKey.ChildIndexEnd;
451HDPublicKey.ChainCodeEnd = HDPublicKey.ChainCodeStart + HDPublicKey.ChainCodeSize;
452HDPublicKey.PublicKeyStart = HDPublicKey.ChainCodeEnd;
453HDPublicKey.PublicKeyEnd = HDPublicKey.PublicKeyStart + HDPublicKey.PublicKeySize;
454HDPublicKey.ChecksumStart = HDPublicKey.PublicKeyEnd;
455HDPublicKey.ChecksumEnd = HDPublicKey.ChecksumStart + HDPublicKey.CheckSumSize;
456
457assert(HDPublicKey.PublicKeyEnd === HDPublicKey.DataSize);
458assert(HDPublicKey.ChecksumEnd === HDPublicKey.SerializedByteSize);
459
460module.exports = HDPublicKey;