UNPKG

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