UNPKG

9.35 kBPlain TextView Raw
1/**
2 * @hidden
3 */
4
5/**
6 */
7//
8// Keychains Object
9// BitGo accessor to a user's keychain.
10//
11// Copyright 2014, BitGo, Inc. All Rights Reserved.
12//
13
14import { randomBytes } from 'crypto';
15import * as common from './common';
16import { Util } from './v2/internal/util';
17import * as bitcoin from '@bitgo/utxo-lib';
18import { hdPath } from './bitcoin';
19const _ = require('lodash');
20let ethereumUtil;
21import * as Bluebird from 'bluebird';
22const co = Bluebird.coroutine;
23
24try {
25 ethereumUtil = require('ethereumjs-util');
26} catch (e) {
27 // ethereum currently not supported
28}
29
30//
31// Constructor
32//
33const Keychains = function(bitgo) {
34 this.bitgo = bitgo;
35};
36
37//
38// isValid
39// Tests a xpub or xprv string to see if it is a valid keychain.
40//
41Keychains.prototype.isValid = function(params) {
42 params = params || {};
43 common.validateParams(params, [], []);
44
45 if (params.ethAddress) {
46 if (!_.isString(params.ethAddress)) {
47 throw new Error('ethAddress must be a string');
48 }
49 return ethereumUtil.isValidAddress(params.ethAddress);
50 }
51
52 if (!_.isString(params.key) && !_.isObject(params.key)) {
53 throw new Error('key must be a string or object');
54 }
55
56 try {
57 if (!params.key.path) {
58 bitcoin.HDNode.fromBase58(params.key);
59 } else {
60 const hdnode = bitcoin.HDNode.fromBase58(params.key.xpub);
61 hdPath(hdnode).derive(params.key.path);
62 }
63 return true;
64 } catch (e) {
65 return false;
66 }
67};
68
69//
70// create
71// Create a new keychain locally.
72// Does not send the keychain to bitgo, only creates locally.
73// If |seed| is provided, used to seed the keychain. Otherwise,
74// a random keychain is created.
75//
76Keychains.prototype.create = function(params) {
77 params = params || {};
78 common.validateParams(params, [], []);
79
80 let seed;
81 if (!params.seed) {
82 // An extended private key has both a normal 256 bit private key and a 256
83 // bit chain code, both of which must be random. 512 bits is therefore the
84 // maximum entropy and gives us maximum security against cracking.
85 seed = randomBytes(512 / 8);
86 } else {
87 seed = params.seed;
88 }
89
90 const extendedKey = bitcoin.HDNode.fromSeedBuffer(seed);
91 const xpub = extendedKey.neutered().toBase58();
92
93 let ethAddress;
94 try {
95 ethAddress = Util.xpubToEthAddress(xpub);
96 } catch (e) {
97 // ethereum is unavailable
98 }
99
100 return {
101 xpub: xpub,
102 xprv: extendedKey.toBase58(),
103 ethAddress: ethAddress
104 };
105};
106
107// used by deriveLocal
108const apiResponse = function(status, result, message) {
109 const err: any = new Error(message);
110 err.status = status;
111 err.result = result;
112 return err;
113};
114
115//
116// deriveLocal
117// Locally derives a keychain from a top level BIP32 string, given a path.
118//
119Keychains.prototype.deriveLocal = function(params) {
120 params = params || {};
121 common.validateParams(params, ['path'], ['xprv', 'xpub']);
122
123 if (!params.xprv && !params.xpub) {
124 throw new Error('must provide an xpub or xprv for derivation.');
125 }
126 if (params.xprv && params.xpub) {
127 throw new Error('cannot provide both xpub and xprv');
128 }
129
130 let hdNode;
131 try {
132 hdNode = bitcoin.HDNode.fromBase58(params.xprv || params.xpub);
133 } catch (e) {
134 throw apiResponse(400, {}, 'Unable to parse the xprv or xpub');
135 }
136
137 let derivedNode;
138 try {
139 derivedNode = hdPath(hdNode).derive(params.path);
140 } catch (e) {
141 throw apiResponse(400, {}, 'Unable to derive HD key from path');
142 }
143
144 const xpub = derivedNode.neutered().toBase58();
145
146 let ethAddress;
147 try {
148 ethAddress = Util.xpubToEthAddress(xpub);
149 } catch (e) {
150 // ethereum is unavailable
151 }
152
153 return {
154 path: params.path,
155 xpub: xpub,
156 xprv: params.xprv && derivedNode.toBase58(),
157 ethAddress: ethAddress
158 };
159};
160
161//
162// list
163// List the user's keychains
164//
165Keychains.prototype.list = function(params, callback) {
166 params = params || {};
167 common.validateParams(params, [], [], callback);
168
169 return this.bitgo.get(this.bitgo.url('/keychain'))
170 .result('keychains')
171 .then(function(keychains) {
172 keychains.map(function(keychain) {
173 if (keychain.xpub && keychain.ethAddress && Util.xpubToEthAddress && keychain.ethAddress !== Util.xpubToEthAddress(keychain.xpub)) {
174 throw new Error('ethAddress and xpub do not match');
175 }
176 });
177 return keychains;
178 })
179 .nodeify(callback);
180};
181
182/**
183 * iterates through all keys associated with the user, decrypts them with the old password and encrypts them with the
184 * new password
185 * @param params.oldPassword {String} - The old password used for encrypting the key
186 * @param params.newPassword {String} - The new password to be used for encrypting the key
187 * @param callback
188 * @returns result.keychains {Object} - e.g.:
189 * {
190 * xpub1: encryptedPrv1,
191 * xpub2: encryptedPrv2,
192 * ...
193 * }
194 * @returns result.version {Number}
195 */
196Keychains.prototype.updatePassword = function(params, callback) {
197 return co(function *coUpdatePassword() {
198 common.validateParams(params, ['oldPassword', 'newPassword'], [], callback);
199 const encrypted = yield this.bitgo.post(this.bitgo.url('/user/encrypted')).result();
200 const newKeychains = {};
201 const self = this;
202 _.forOwn((encrypted as any).keychains, function keychainsForOwn(oldEncryptedXprv, xpub) {
203 try {
204 const decryptedPrv = self.bitgo.decrypt({ input: oldEncryptedXprv, password: params.oldPassword });
205 const newEncryptedPrv = self.bitgo.encrypt({ input: decryptedPrv, password: params.newPassword });
206 newKeychains[xpub] = newEncryptedPrv;
207 } catch (e) {
208 // decrypting the keychain with the old password didn't work so we just keep it the way it is
209 newKeychains[xpub] = oldEncryptedXprv;
210 }
211 });
212 return { keychains: newKeychains, version: (encrypted as any).version };
213 }).call(this).asCallback(callback);
214};
215
216//
217// add
218// Add a new keychain
219//
220Keychains.prototype.add = function(params, callback) {
221 params = params || {};
222 common.validateParams(params, ['xpub'], ['encryptedXprv', 'type', 'isLedger'], callback);
223
224 return this.bitgo.post(this.bitgo.url('/keychain'))
225 .send({
226 xpub: params.xpub,
227 encryptedXprv: params.encryptedXprv,
228 type: params.type,
229 originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
230 isLedger: params.isLedger
231 })
232 .result()
233 .then(function(keychain) {
234 if (keychain.xpub && keychain.ethAddress && Util.xpubToEthAddress && keychain.ethAddress !== Util.xpubToEthAddress(keychain.xpub)) {
235 throw new Error('ethAddress and xpub do not match');
236 }
237 return keychain;
238 })
239 .nodeify(callback);
240};
241
242//
243// createBitGo
244// Add a new BitGo server keychain
245//
246Keychains.prototype.createBitGo = function(params, callback) {
247 params = params || {};
248 common.validateParams(params, [], [], callback);
249
250 return this.bitgo.post(this.bitgo.url('/keychain/bitgo'))
251 .send(params)
252 .result()
253 .then(function(keychain) {
254 if (keychain.xpub && keychain.ethAddress && Util.xpubToEthAddress && keychain.ethAddress !== Util.xpubToEthAddress(keychain.xpub)) {
255 throw new Error('ethAddress and xpub do not match');
256 }
257 return keychain;
258 })
259 .nodeify(callback);
260};
261
262//
263// createBackup
264// Create a new backup keychain through bitgo - often used for creating a keychain on a KRS
265//
266Keychains.prototype.createBackup = function(params, callback) {
267 params = params || {};
268 common.validateParams(params, ['provider'], [], callback);
269
270 return this.bitgo.post(this.bitgo.url('/keychain/backup'))
271 .send(params)
272 .result()
273 .then(function(keychain) {
274 // not all keychains have an xpub
275 if (keychain.xpub && keychain.ethAddress && Util.xpubToEthAddress && keychain.ethAddress !== Util.xpubToEthAddress(keychain.xpub)) {
276 throw new Error('ethAddress and xpub do not match');
277 }
278 return keychain;
279 })
280 .nodeify(callback);
281};
282
283//
284// get
285// Fetch an existing keychain
286// Parameters include:
287// xpub: the xpub of the key to lookup (required)
288//
289Keychains.prototype.get = function(params, callback) {
290 params = params || {};
291 common.validateParams(params, [], ['xpub', 'ethAddress'], callback);
292
293 if (!params.xpub && !params.ethAddress) {
294 throw new Error('xpub or ethAddress must be defined');
295 }
296
297 const id = params.xpub || params.ethAddress;
298 return this.bitgo.post(this.bitgo.url('/keychain/' + encodeURIComponent(id)))
299 .send({})
300 .result()
301 .then(function(keychain) {
302 if (keychain.xpub && keychain.ethAddress && Util.xpubToEthAddress && keychain.ethAddress !== Util.xpubToEthAddress(keychain.xpub)) {
303 throw new Error('ethAddress and xpub do not match');
304 }
305 return keychain;
306 })
307 .nodeify(callback);
308};
309
310//
311// update
312// Update an existing keychain
313// Parameters include:
314// xpub: the xpub of the key to lookup (required)
315//
316Keychains.prototype.update = function(params, callback) {
317 params = params || {};
318 common.validateParams(params, ['xpub'], ['encryptedXprv'], callback);
319
320 return this.bitgo.put(this.bitgo.url('/keychain/' + params.xpub))
321 .send({
322 encryptedXprv: params.encryptedXprv
323 })
324 .result()
325 .then(function(keychain) {
326 if (keychain.xpub && keychain.ethAddress && Util.xpubToEthAddress && keychain.ethAddress !== Util.xpubToEthAddress(keychain.xpub)) {
327 throw new Error('ethAddress and xpub do not match');
328 }
329 return keychain;
330 })
331 .nodeify(callback);
332};
333
334
335module.exports = Keychains;