Skip to content

Commit 31131f8

Browse files
author
Arik Sosman
committed
add support for Bitcash signatures
Reviewers: benchan, alex Reviewed By: alex Subscribers: ben Differential Revision: https://phabricator.bitgo.com/D6112
1 parent 5e9ba3e commit 31131f8

File tree

6 files changed

+256
-66
lines changed

6 files changed

+256
-66
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bitgo",
3-
"version": "3.4.12",
3+
"version": "3.4.13",
44
"description": "BitGo Javascript SDK",
55
"main": "./src/index.js",
66
"keywords": [
@@ -72,6 +72,7 @@
7272
"bigi": "1.4.0",
7373
"bignumber.js": "~4.0.2",
7474
"bitcoinjs-lib": "2.1.4",
75+
"bitgoinjs-lib": "git+ssh://[email protected]:BitGo/bitgoinjs-lib.git#pr-831",
7576
"body-parser": "~1.17.2",
7677
"bs58": "2.0.1",
7778
"bs58check": "1.0.4",

src/bitgo.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
var superagent = require('superagent');
88
var bitcoin = require('./bitcoin');
9+
require('./bitgoin'); // this amends hdPath capabilities
910
var sanitizeHtml = require('sanitize-html');
1011
var eol = require('eol');
1112
var BaseCoin = require('./v2/baseCoin');
@@ -38,7 +39,9 @@ if (!process.browser) {
3839
var _end = superagent.Request.prototype.end;
3940
superagent.Request.prototype.end = function(cb) {
4041
var self = this;
41-
if (typeof cb === 'function') return _end.call(self, cb);
42+
if (typeof cb === 'function') {
43+
return _end.call(self, cb);
44+
}
4245

4346
return new Q.Promise(function(resolve, reject) {
4447
var error;
@@ -68,7 +71,7 @@ var handleResponseResult = function(optionalField) {
6871
return optionalField ? res.body[optionalField] : res.body;
6972
}
7073
throw errFromResponse(res);
71-
}
74+
};
7275
};
7376

7477
var errFromResponse = function(res) {
@@ -153,7 +156,7 @@ var BitGo = function(params) {
153156
// Deprecate useProduction in the future
154157
if (params.useProduction) {
155158
if (params.env && params.env !== 'prod') {
156-
throw new Error("Cannot set test environment and use production");
159+
throw new Error('Cannot set test environment and use production');
157160
}
158161
params.env = 'prod';
159162
}
@@ -272,7 +275,7 @@ var BitGo = function(params) {
272275
var contentType = this.getHeader('Content-Type');
273276
// Parse out just the content type from the header (ignore the charset)
274277
if (contentType) {
275-
contentType = contentType.split(';')[0]
278+
contentType = contentType.split(';')[0];
276279
}
277280
var serialize = superagent.serialize[contentType];
278281
if (!serialize && isJSON(contentType)) {
@@ -1013,15 +1016,15 @@ BitGo.prototype.addAccessToken = function(params, callback) {
10131016
throw new Error('txValueLimit must be a number');
10141017
}
10151018
if (params.txValueLimit < 0) {
1016-
throw new Error('txValueLimit must be a non-negative number')
1019+
throw new Error('txValueLimit must be a non-negative number');
10171020
}
10181021
}
1019-
if(params.scope && params.scope.length > 0) {
1020-
if(!_.isArray(params.scope)) {
1022+
if (params.scope && params.scope.length > 0) {
1023+
if (!_.isArray(params.scope)) {
10211024
throw new Error('scope must be an array');
1022-
}
1025+
}
10231026
} else {
1024-
throw new Error('must specify scope for token')
1027+
throw new Error('must specify scope for token');
10251028
}
10261029

10271030
var bitgo = this;
@@ -1080,7 +1083,7 @@ BitGo.prototype.removeAccessToken = function(params, callback) {
10801083
common.validateParams(params, [], ['id', 'label'], callback);
10811084
var exactlyOne = !!params.id ^ !!params.label;
10821085
if (!exactlyOne) {
1083-
throw new Error('must provide exactly one of id or label')
1086+
throw new Error('must provide exactly one of id or label');
10841087
}
10851088

10861089
var self = this;

src/bitgoin.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
var common = require('./common');
2+
var bitcoin = require('bitgoinjs-lib');
3+
var ecurve = require('ecurve');
4+
var curve = ecurve.getCurveByName('secp256k1');
5+
var BigInteger = require('bigi');
6+
var createHmac = require('create-hmac');
7+
var HDNode = bitcoin.HDNode;
8+
9+
var secp256k1;
10+
11+
try {
12+
secp256k1 = require('secp256k1');
13+
} catch (err) {
14+
console.log('running without secp256k1 acceleration');
15+
}
16+
17+
// Check for IE, and disable secp256k1, due to:
18+
// https://github.com/indutny/bn.js/issues/133
19+
var isIE = (({}).constructor.name === undefined);
20+
if (isIE) {
21+
secp256k1 = undefined;
22+
}
23+
24+
bitcoin.getNetwork = function(network) {
25+
network = network || common.getNetwork();
26+
return bitcoin.networks[network];
27+
};
28+
29+
bitcoin.makeRandomKey = function() {
30+
return bitcoin.ECPair.makeRandom({ network: bitcoin.getNetwork() });
31+
};
32+
33+
HDNode.prototype.getKey = function(network) {
34+
network = network || bitcoin.getNetwork();
35+
var k = this.keyPair;
36+
var result = new bitcoin.ECPair(k.d, k.d ? null : k.Q, { network: network, compressed: k.compressed });
37+
// Creating Q from d takes ~25ms, so if it's not created, use native bindings to pre-compute
38+
if (!result.__Q && secp256k1) {
39+
result.__Q = ecurve.Point.decodeFrom(curve, secp256k1.publicKeyCreate(k.d.toBuffer(32), false));
40+
}
41+
return result;
42+
};
43+
44+
/**
45+
* Derive a child HDNode from a parent HDNode and index. Uses secp256k1 to speed
46+
* up public key derivations by 100x vs. bitcoinjs-lib implementation.
47+
*
48+
* @param {HDNode} hdnode parent HDNode
49+
* @param {Number} index child index
50+
* @returns {HDNode} derived HDNode
51+
*/
52+
var deriveFast = function (hdnode, index) {
53+
// no fast path for private key derivations -- delegate to standard method
54+
if (!secp256k1 || hdnode.keyPair.d) {
55+
return hdnode.derive(index);
56+
}
57+
58+
var isHardened = index >= bitcoin.HDNode.HIGHEST_BIT;
59+
if (isHardened) {
60+
throw new Error('cannot derive hardened key from public key');
61+
}
62+
63+
var indexBuffer = new Buffer(4);
64+
indexBuffer.writeUInt32BE(index, 0);
65+
66+
var data;
67+
68+
// data = serP(point(kpar)) || ser32(index)
69+
// = serP(Kpar) || ser32(index)
70+
data = Buffer.concat([
71+
hdnode.keyPair.getPublicKeyBuffer(),
72+
indexBuffer
73+
]);
74+
75+
var I = createHmac('sha512', hdnode.chainCode).update(data).digest();
76+
var IL = I.slice(0, 32);
77+
var IR = I.slice(32);
78+
79+
var pIL = BigInteger.fromBuffer(IL);
80+
81+
// In case parse256(IL) >= n, proceed with the next value for i
82+
if (pIL.compareTo(curve.n) >= 0) {
83+
return deriveFast(hdnode, index + 1);
84+
}
85+
86+
// Private parent key -> private child key
87+
var hd;
88+
// Ki = point(parse256(IL)) + Kpar
89+
// = G*IL + Kpar
90+
91+
// The expensive op is the point multiply -- use secp256k1 lib to do that
92+
var Ki = ecurve.Point.decodeFrom(curve, secp256k1.publicKeyCreate(IL, false)).add(hdnode.keyPair.Q);
93+
94+
// In case Ki is the point at infinity, proceed with the next value for i
95+
if (curve.isInfinity(Ki)) {
96+
return deriveFast(hdnode, index + 1);
97+
}
98+
99+
var keyPair = new bitcoin.ECPair(null, Ki, { network: hdnode.keyPair.network });
100+
hd = new bitcoin.HDNode(keyPair, IR);
101+
102+
hd.depth = hdnode.depth + 1;
103+
hd.index = index;
104+
hd.parentFingerprint = hdnode.getFingerprint().readUInt32BE(0);
105+
106+
return hd;
107+
};
108+
109+
if (secp256k1) {
110+
bitcoin.ECPair.prototype.sign = function (hash) {
111+
if (!this.d) {
112+
throw new Error('Missing private key');
113+
}
114+
var sig = secp256k1.sign(hash, this.d.toBuffer(32)).signature;
115+
return bitcoin.ECSignature.fromDER(secp256k1.signatureExport(sig));
116+
};
117+
118+
bitcoin.ECPair.prototype.verify = function (hash, signature) {
119+
signature = new bitcoin.ECSignature(signature.r, signature.s);
120+
signature = secp256k1.signatureNormalize(secp256k1.signatureImport(signature.toDER()));
121+
return secp256k1.verify(hash, signature, this.getPublicKeyBuffer());
122+
};
123+
}
124+
125+
/**
126+
* Derive a BIP32 path, given a root key
127+
* We cache keys at each level of hierarchy we derive, to avoid re-deriving (approx 25ms per derivation)
128+
* @param rootKey key to derive off
129+
* @param path the path, e.g. 'm/0/0/0/1'
130+
* @returns {*} the derived hd key
131+
*/
132+
bitcoin.hdPath = function(rootKey) {
133+
var cache = {};
134+
var derive = function (path) {
135+
var components = path.split('/').filter(function (c) {
136+
return c !== '';
137+
});
138+
// strip any extraneous / characters
139+
path = components.join('/');
140+
if (cache[path]) {
141+
return cache[path];
142+
}
143+
var len = components.length;
144+
if (len === 0 || len === 1 && components[0] === 'm') {
145+
return rootKey;
146+
}
147+
var parentPath = components.slice(0, len - 1).join('/');
148+
var parentKey = derive(parentPath);
149+
var el = components[len - 1];
150+
151+
var hardened = false;
152+
if (el[el.length - 1] === "'") {
153+
hardened = true;
154+
}
155+
var index = parseInt(el);
156+
var derived;
157+
if (hardened) {
158+
derived = parentKey.deriveHardened(index);
159+
} else {
160+
derived = deriveFast(parentKey, index);
161+
}
162+
cache[path] = derived;
163+
return derived;
164+
};
165+
166+
var deriveKey = function(path) {
167+
var hdNode = this.derive(path);
168+
return hdNode.keyPair;
169+
};
170+
171+
return {
172+
derive: derive,
173+
deriveKey: deriveKey
174+
};
175+
};
176+
177+
module.exports = bitcoin;

src/v2/baseCoin.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ var BaseCoin = function(bitgo, coin) {
1616
var self = this;
1717
this.type = coin;
1818

19-
this.url = function(suffix) {
20-
return bitgo._baseUrl + '/api/v2/' + coin + suffix;
19+
this.url = (suffix) => {
20+
return bitgo._baseUrl + '/api/v2/' + this.getChain() + suffix;
2121
};
2222

2323
this.wallets = function() {
@@ -48,7 +48,7 @@ var BaseCoin = function(bitgo, coin) {
4848
self.coinPendingApprovals = new PendingApprovals(bitgo, this);
4949
}
5050
return self.coinPendingApprovals;
51-
}
51+
};
5252
};
5353

5454
BaseCoin.prototype.initializeCoin = function(coin) {
@@ -58,6 +58,7 @@ BaseCoin.prototype.initializeCoin = function(coin) {
5858
btc: require('./coins/btc'),
5959
tbtc: require('./coins/tbtc'),
6060
bch: require('./coins/bch'),
61+
tbch: require('./coins/tbch'),
6162
ltc: require('./coins/ltc'),
6263
tltc: require('./coins/tltc'),
6364
eth: require('./coins/eth'),
@@ -137,7 +138,7 @@ BaseCoin.prototype.initiateRecovery = function(params) {
137138
const userHDNode = prova.HDNode.fromBase58(userKey);
138139
return Q(userHDNode);
139140
} catch (e) {
140-
throw new Error("Failed to decrypt user key with passcode - try again!");
141+
throw new Error('Failed to decrypt user key with passcode - try again!');
141142
}
142143
};
143144

@@ -156,20 +157,20 @@ BaseCoin.prototype.initiateRecovery = function(params) {
156157
const backupHDNode = prova.HDNode.fromBase58(backupKey);
157158
keys.push(backupHDNode);
158159
} catch (e) {
159-
throw new Error("Failed to decrypt backup key with passcode - try again!");
160+
throw new Error('Failed to decrypt backup key with passcode - try again!');
160161
}
161162
try {
162163
const bitgoHDNode = prova.HDNode.fromBase58(bitgoXpub);
163164
keys.push(bitgoHDNode);
164165
} catch (e) {
165166
if (self.getFamily() !== 'xrp') {
166167
// in XRP recoveries, the BitGo xpub is optional
167-
throw new Error("Failed to parse bitgo xpub!");
168+
throw new Error('Failed to parse bitgo xpub!');
168169
}
169170
}
170171
// Validate the destination address
171172
if (!self.isValidAddress(destinationAddress)) {
172-
throw new Error("Invalid destination address!");
173+
throw new Error('Invalid destination address!');
173174
}
174175

175176
return keys;

0 commit comments

Comments
 (0)