From c3a64761b651b91a743008e0e533c1471b098a95 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sat, 28 Feb 2015 21:12:03 -0300 Subject: [PATCH 01/13] refactor client --- lib/client/airgapped.js | 46 ++ lib/client/api.js | 846 ++++++++++++---------------------- lib/client/credentials.js | 110 +++++ lib/client/filestorage.js | 33 -- lib/client/index.js | 4 +- lib/client/verifier.js | 20 +- lib/expressapp.js | 4 - lib/model/copayer.js | 6 - lib/model/txproposalaction.js | 2 +- lib/server.js | 9 +- lib/walletutils.js | 6 +- test/integration/clientApi.js | 840 +++++++++++---------------------- 12 files changed, 736 insertions(+), 1190 deletions(-) create mode 100644 lib/client/airgapped.js create mode 100644 lib/client/credentials.js delete mode 100644 lib/client/filestorage.js diff --git a/lib/client/airgapped.js b/lib/client/airgapped.js new file mode 100644 index 0000000..14408fc --- /dev/null +++ b/lib/client/airgapped.js @@ -0,0 +1,46 @@ +'use strict'; + +var _ = require('lodash'); +var $ = require('preconditions').singleton(); +var util = require('util'); +var async = require('async'); +var log = require('npmlog'); +var events = require('events'); +log.debug = log.verbose; +var Bitcore = require('bitcore') + +var Credentials = require('./credentials'); +var WalletUtils = require('../walletutils'); +var Verifier = require('./verifier'); +var ServerCompromisedError = require('./servercompromisederror'); +var ClientError = require('../clienterror'); + +function AirGapped(opts) { + this.verbose = !!opts.verbose; + if (this.verbose) { + log.level = 'debug'; + } else { + log.level = 'info'; + } + this.credentials = Credentials.create(opts.network || 'livenet'); +}; + +util.inherits(AirGapped, events.EventEmitter); + +AirGapped.prototype.getSeed = function() { + var cred = this.credentials; + + return { + network: cred.network, + xPubKey: cred.xPubKey, + requestPrivKey: cred.requestPrivKey, + }; +}; + +AirGapped.prototype.signTxProposals = function(txps, cb) { + return cb(null, _.map(txps, function(txp) { + return {}; + })); +}; + +module.exports = AirGapped; diff --git a/lib/client/api.js b/lib/client/api.js index 2abb69c..081b833 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -8,8 +8,9 @@ var log = require('npmlog'); var request = require('request') var events = require('events'); log.debug = log.verbose; - var Bitcore = require('bitcore') + +var Credentials = require('./credentials'); var WalletUtils = require('../walletutils'); var Verifier = require('./verifier'); var ServerCompromisedError = require('./servercompromisederror'); @@ -17,11 +18,6 @@ var ClientError = require('../clienterror'); var BASE_URL = 'http://localhost:3001/copay/api'; -var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'n', 'publicKeyRing', 'sharedEncryptingKey']; -var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; - -var WALLET_AIRGAPPED_TOCOMPLETE = ['publicKeyRing', 'm', 'n', 'sharedEncryptingKey']; - function _encryptMessage(message, encryptingKey) { if (!message) return null; return WalletUtils.encryptMessage(message, encryptingKey); @@ -75,45 +71,13 @@ function _signRequest(method, url, args, privKey) { return WalletUtils.signMessage(message, privKey); }; -function _initWcd(network) { - $.checkArgument(network); - var xPrivKey = new Bitcore.HDPrivateKey(network); - var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); - var roPrivKey = xPrivKey.derive('m/1/0').privateKey; - var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; - var copayerId = WalletUtils.xPubToCopayerId(xPubKey); - - - return { - copayerId: copayerId, - xPrivKey: xPrivKey.toString(), - publicKeyRing: [xPubKey], - network: network, - roPrivKey: roPrivKey.toWIF(), - rwPrivKey: rwPrivKey.toWIF(), - }; -}; - -function _addWalletToWcd(wcd, walletPrivKey, m, n) { - $.checkArgument(wcd); - var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); - - wcd.walletPrivKey = walletPrivKey.toWIF(); - wcd.sharedEncryptingKey = sharedEncryptingKey; - wcd.m = m; - wcd.n = n; -}; - function API(opts) { - if (!opts.storage) { - throw new Error('Must provide storage option'); - } - this.storage = opts.storage; + opts = opts || {}; + this.verbose = !!opts.verbose; - this.request = request || opts.request; + this.request = opts.request || request; this.baseUrl = opts.baseUrl || BASE_URL; this.basePath = this.baseUrl.replace(/http.?:\/\/[a-zA-Z0-9:-]*\//, '/'); - this.noPasswdAccess = opts.noPasswdAccess || 'full'; if (this.verbose) { log.level = 'debug'; } else { @@ -123,176 +87,17 @@ function API(opts) { util.inherits(API, events.EventEmitter); -API.prototype._tryToCompleteFromServer = function(wcd, cb) { - - if (!wcd.walletPrivKey) - return cb('Could not perform that action. Wallet Incomplete'); - - var self = this; - var url = '/v1/wallets/'; - self._doGetRequest(url, wcd, function(err, ret) { - if (err) return cb(err); - var wallet = ret.wallet; - - if (wallet.status != 'complete') - return cb('Wallet Incomplete'); - - if (!Verifier.checkCopayers(wallet.copayers, wcd.walletPrivKey, - wcd.xPrivKey, wcd.n)) { - - return cb(new ServerCompromisedError( - 'Copayers in the wallet could not be verified to have known the wallet secret')); - } - - wcd.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') - - self.save(wcd, function(err) { - return cb(err, wcd); - }); - }); -}; - - -API.prototype._tryToCompleteFromData = function(wcd, toComplete, cb) { - var inData = _decryptMessage(toComplete, - WalletUtils.privateKeyToAESKey(wcd.roPrivKey)); - if (!inData) - return cb('Could not complete wallet'); - - try { - inData = JSON.parse(inData); - _.extend(wcd, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); - } catch (ex) { - return cb(ex); - } - - this.save(wcd, function(err) { - return cb(err, wcd); - }); -}; - - -API.prototype._tryToComplete = function(opts, wcd, cb) { - if (opts.toComplete) { - this._tryToCompleteFromData(wcd, opts.toComplete, cb); - } else { - this._tryToCompleteFromServer(wcd, cb); - } -}; - - -// access: 'full' > 'readwrite' > readonly' -API.prototype._processWcdAfterRead = function(rawData, requiredAccess, cb) { - var WU = WalletUtils; - requiredAccess = requiredAccess || 'full'; - - if (!rawData) - return cb(null, rawData); - - var requiredAccessLevel = WU.accessNameToLevel(requiredAccess); - - var access = WU.accessFromData(rawData); - var accessLevel = WU.accessNameToLevel(access); - - // Is the data available? - if (requiredAccessLevel <= accessLevel) - return cb(null, rawData); - - // Has any encrypted info? - if (!rawData.enc) - return cb('NOTAUTH'); - - // Decrypt it and try again - this.emit('needPassword', function(password) { - if (!password) return cb('No password'); - - try { - rawData = WU.decryptWallet(rawData, password); - } catch (e) {}; - - if (!rawData) - return cb('NOTAUTH'); - - access = WU.accessFromData(rawData); - accessLevel = WU.accessNameToLevel(access); - - // Is the data available? - if (requiredAccessLevel <= accessLevel) - return cb(null, rawData); - - return cb('NOTAUTH'); - }); -}; - - -API.prototype.setNopasswdAccess = function(noPasswdAccess) { - if (!_.contains(['none', 'readonly', 'readwrite', 'full'], noPasswdAccess)) - throw new Error('Bad nopasswd access:' + noPasswdAccess); - - this.noPasswdAccess = noPasswdAccess; +API.prototype.seedFromAirGapped = function(seed) { + this.credentials = Credentials.fromAirGapped(seed.network, seed.xPubKey, seed.requestPrivKey); }; -API.prototype._processWcdBeforeWrite = function(wcd, cb) { - var self = this; +API.prototype._doRequest = function(method, url, args, cb) { + $.checkState(this.credentials); - // Is any encrypted? - if (this.noPasswdAccess == 'full') { - return cb(null, wcd); - } else { - this.emit('needNewPassword', function(password) { - if (!password) return cb('No password given'); - var ewcd = WalletUtils.encryptWallet(wcd, self.noPasswdAccess, password); - return cb(null, ewcd); - }); - } -}; - - - -API.prototype._load = function(opts, cb) { - var self = this; - $.shouldBeFunction(cb); - - this.storage.load(function(err, rawdata) { - if (err || !rawdata) { - if (err && err.code == 'ENOENT') err = 'NOTFOUND'; - return cb(err || 'NOTFOUND'); - } - - self._processWcdAfterRead(rawdata, opts.requiredAccess, cb); - }); -}; - - -/** - * _loadAndCheck - * - * @param opts.pkr - */ -API.prototype._loadAndCheck = function(opts, cb) { - var self = this; - - this._load(opts, function(err, wcd) { - if (err) return cb(err); - - if (!wcd.n || (wcd.n > 1 && wcd.publicKeyRing.length != wcd.n)) { - return self._tryToComplete(opts, wcd, cb); - } - - return cb(null, wcd); - }); -}; - -API.prototype._doRequest = function(method, url, args, wcd, cb) { var reqSignature; - wcd = wcd || {}; - if (method == 'get') { - if (wcd.roPrivKey) - reqSignature = _signRequest(method, url, args, wcd.roPrivKey); - } else { - if (wcd.rwPrivKey) - reqSignature = _signRequest(method, url, args, wcd.rwPrivKey); + if (this.credentials.requestPrivKey) { + reqSignature = _signRequest(method, url, args, this.credentials.requestPrivKey); } var absUrl = this.baseUrl + url; @@ -300,7 +105,7 @@ API.prototype._doRequest = function(method, url, args, wcd, cb) { // relUrl: only for testing with `supertest` relUrl: this.basePath + url, headers: { - 'x-identity': wcd.copayerId, + 'x-identity': this.credentials.copayerId, 'x-signature': reqSignature, }, method: method, @@ -308,6 +113,7 @@ API.prototype._doRequest = function(method, url, args, wcd, cb) { body: args, json: true, }; + log.verbose('Request Args', util.inspect(args, { depth: 10 })); @@ -326,12 +132,12 @@ API.prototype._doRequest = function(method, url, args, wcd, cb) { }; -API.prototype._doPostRequest = function(url, args, wcd, cb) { - return this._doRequest('post', url, args, wcd, cb); +API.prototype._doPostRequest = function(url, args, cb) { + return this._doRequest('post', url, args, cb); }; -API.prototype._doGetRequest = function(url, wcd, cb) { - return this._doRequest('get', url, {}, wcd, cb); +API.prototype._doGetRequest = function(url, cb) { + return this._doRequest('get', url, {}, cb); }; @@ -343,166 +149,145 @@ API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayer xPubKeySignature: WalletUtils.signMessage(xPubKey, walletPrivKey), }; var url = '/v1/wallets/' + walletId + '/copayers'; - this._doPostRequest(url, args, {}, function(err, body) { + this._doPostRequest(url, args, function(err, body) { if (err) return cb(err); return cb(null, body.wallet); }); }; -API.prototype.save = function(inWcd, cb) { - var self = this; - - self._processWcdBeforeWrite(inWcd, function(err, wcd) { - if (err) return cb(err); +API.prototype.isComplete = function() { + return this.credentials && this.credentials.isComplete(); +}; - self.storage.save(wcd, function(err) { - return cb(err, null); - }); - }); -} +/** + * Opens a wallet and tries to complete the public key ring. + * @param {Function} cb - Returns an error and a flag indicating that the wallet has just been completed and needs to be persisted + */ +API.prototype.openWallet = function(cb) { + $.checkState(this.credentials); -API.prototype.generateKey = function(network, cb) { var self = this; - network = network || 'livenet'; - if (!_.contains(['testnet', 'livenet'], network)) - return cb('Invalid network'); - this.storage.load(function(err, wcd) { - if (wcd) - return cb(self.storage.getName() + ' already contains a wallet'); + if (self.credentials.isComplete()) return cb(null, false); - var wcd = _initWcd(network); + var url = '/v1/wallets/'; + self._doGetRequest(url, function(err, ret) { + if (err) return cb(err); + var wallet = ret.wallet; - self.save(wcd, function(err) { - return cb(err, null); - }); + if (wallet.status != 'complete') + return cb('Wallet Incomplete'); + + if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { + return cb(new ServerCompromisedError( + 'Copayers in the wallet could not be verified to have known the wallet secret')); + } + + self.credentials.addPublicKeyRing(_.pluck(wallet.copayers, 'xPubKey')); + return cb(null, true); }); }; API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) { var self = this; + network = network || 'livenet'; if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); - this._load({ - requiredAccess: 'readonly', - }, function(err, wcd) { - if (err && err != 'NOTFOUND') - return cb(err); - - if (wcd && wcd.n) - return cb(self.storage.getName() + ' already contains a wallet'); - - if (wcd && wcd.network && wcd.network != network) - return cb('Storage ' + self.storage.getName() + ' is set to network:' + wcd.network); - - var walletPrivKey = new Bitcore.PrivateKey(); - var args = { - name: walletName, - m: m, - n: n, - pubKey: walletPrivKey.toPublicKey().toString(), - network: network, - }; - var url = '/v1/wallets/'; - self._doPostRequest(url, args, {}, function(err, body) { - if (err) return cb(err); + if (!self.credentials) { + self.credentials = Credentials.create(network); + } - var walletId = body.walletId; + $.checkState(network == self.credentials.network); - var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); + var walletPrivKey = new Bitcore.PrivateKey(); + var args = { + name: walletName, + m: m, + n: n, + pubKey: walletPrivKey.toPublicKey().toString(), + network: network, + }; + var url = '/v1/wallets/'; + self._doPostRequest(url, args, function(err, body) { + if (err) return cb(err); - wcd = wcd || _initWcd(network); - _addWalletToWcd(wcd, walletPrivKey, m, n) + var walletId = body.walletId; - self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[0], copayerName, - function(err, wallet) { - if (err) return cb(err); - self.save(wcd, function(err) { - return cb(err, n > 1 ? secret : null); - }); - }); - }); + var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); + self.credentials.addWalletInfo(walletId, walletName, m, n, walletPrivKey.toString(), copayerName); + + self._doJoinWallet(walletId, walletPrivKey, self.credentials.xPubKey, copayerName, + function(err, wallet) { + if (err) return cb(err); + return cb(null, n > 1 ? secret : null); + }); }); }; -API.prototype.reCreateWallet = function(walletName, cb) { - var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { - if (err) return cb(err); +// API.prototype.reCreateWallet = function(walletName, cb) { +// var self = this; +// this._loadAndCheck(function(err, wcd) { +// if (err) return cb(err); - var walletPrivKey = new Bitcore.PrivateKey(); - var args = { - name: walletName, - m: wcd.m, - n: wcd.n, - pubKey: walletPrivKey.toPublicKey().toString(), - network: wcd.network, - }; - var url = '/v1/wallets/'; - self._doPostRequest(url, args, {}, function(err, body) { - if (err) return cb(err); +// var walletPrivKey = new Bitcore.PrivateKey(); +// var args = { +// name: walletName, +// m: wcd.m, +// n: wcd.n, +// pubKey: walletPrivKey.toPublicKey().toString(), +// network: wcd.network, +// }; +// var url = '/v1/wallets/'; +// self._doPostRequest(url, args, function(err, body) { +// if (err) return cb(err); - var walletId = body.walletId; +// var walletId = body.walletId; - var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network); - var i = 0; - async.each(wcd.publicKeyRing, function(xpub, next) { - var copayerName = 'recovered Copayer #' + i; - self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next); - }, function(err) { - return cb(err); - }); - }); - }); -}; +// var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network); +// var i = 0; +// async.each(wcd.publicKeyRing, function(xpub, next) { +// var copayerName = 'recovered Copayer #' + i; +// self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next); +// }, function(err) { +// return cb(err); +// }); +// }); +// }); +// }; API.prototype.joinWallet = function(secret, copayerName, cb) { var self = this; - this._load({ - requiredAccess: 'readonly' - }, function(err, wcd) { - if (err && err != 'NOTFOUND') - return cb(err); - - if (wcd && wcd.n) - return cb(self.storage.getName() + ' already contains a wallet'); + try { + var secretData = WalletUtils.fromSecret(secret); + } catch (ex) { + return cb(ex); + } - try { - var secretData = WalletUtils.fromSecret(secret); - } catch (ex) { - return cb(ex); - } - wcd = wcd || _initWcd(secretData.network); + if (!self.credentials) { + self.credentials = Credentials.create(secretData.network); + } - self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, wcd.publicKeyRing[0], copayerName, - function(err, joinedWallet) { - if (err) return cb(err); - _addWalletToWcd(wcd, secretData.walletPrivKey, joinedWallet.m, joinedWallet.n); - self.save(wcd, cb); - }); - }); + self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, self.credentials.xPubKey, copayerName, + function(err, wallet) { + if (err) return cb(err); + self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, secretData.walletPrivKey, copayerName); + return cb(null, wallet); + }); }; API.prototype.getStatus = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); var self = this; - this._load({ - requiredAccess: 'readonly' - }, function(err, wcd) { - if (err) return cb(err); - - var url = '/v1/wallets/'; - self._doGetRequest(url, wcd, function(err, result) { - _processTxps(result.pendingTxps, wcd.sharedEncryptingKey); - return cb(err, result, wcd.copayerId); - }); + var url = '/v1/wallets/'; + self._doGetRequest(url, function(err, result) { + _processTxps(result.pendingTxps, self.credentials.sharedEncryptingKey); + return cb(err, result, self.credentials.copayerId); }); }; @@ -515,50 +300,41 @@ API.prototype.getStatus = function(cb) { * @param opts.message */ API.prototype.sendTxProposal = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(opts); $.shouldBeNumber(opts.amount); var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { - if (err) return cb(err); - - if (!wcd.rwPrivKey) - return cb('No key to generate proposals'); - - var args = { - toAddress: opts.toAddress, - amount: opts.amount, - message: _encryptMessage(opts.message, wcd.sharedEncryptingKey), - }; - var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); - args.proposalSignature = WalletUtils.signMessage(hash, wcd.rwPrivKey); - log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); + var args = { + toAddress: opts.toAddress, + amount: opts.amount, + message: _encryptMessage(opts.message, self.credentials.sharedEncryptingKey), + }; + var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); + args.proposalSignature = WalletUtils.signMessage(hash, self.credentials.requestPrivKey); + log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); - var url = '/v1/txproposals/'; - self._doPostRequest(url, args, wcd, cb); + var url = '/v1/txproposals/'; + self._doPostRequest(url, args, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); }); }; API.prototype.createAddress = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({ - requiredAccess: 'readwrite', - }, function(err, wcd) { + var url = '/v1/addresses/'; + self._doPostRequest(url, {}, function(err, address) { if (err) return cb(err); + if (!Verifier.checkAddress(self.credentials, address)) { + return cb(new ServerCompromisedError('Server sent fake address')); + } - var url = '/v1/addresses/'; - self._doPostRequest(url, {}, wcd, function(err, address) { - if (err) return cb(err); - if (!Verifier.checkAddress(wcd, address)) { - return cb(new ServerCompromisedError('Server sent fake address')); - } - - return cb(null, address); - }); + return cb(null, address); }); }; @@ -567,39 +343,31 @@ API.prototype.createAddress = function(cb) { */ API.prototype.getMainAddresses = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { + var url = '/v1/addresses/'; + self._doGetRequest(url, function(err, addresses) { if (err) return cb(err); - var url = '/v1/addresses/'; - self._doGetRequest(url, wcd, function(err, addresses) { - if (err) return cb(err); - - if (!opts.doNotVerify) { - var fake = _.any(addresses, function(address) { - return !Verifier.checkAddress(wcd, address); - }); - if (fake) - return cb(new ServerCompromisedError('Server sent fake address')); - } - return cb(null, addresses); - }); + if (!opts.doNotVerify) { + var fake = _.any(addresses, function(address) { + return !Verifier.checkAddress(self.credentials, address); + }); + if (fake) + return cb(new ServerCompromisedError('Server sent fake address')); + } + return cb(null, addresses); }); }; API.prototype.getBalance = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { - if (err) return cb(err); - var url = '/v1/balance/'; - self._doGetRequest(url, wcd, cb); - }); + var url = '/v1/balance/'; + self._doGetRequest(url, cb); }; /** @@ -608,111 +376,107 @@ API.prototype.getBalance = function(cb) { * * @param opts.access =['full', 'readonly', 'readwrite'] */ -API.prototype.export = function(opts, cb) { - var self = this; - $.shouldBeFunction(cb); - opts = opts || {}; - var access = opts.access || 'full'; +// API.prototype.export = function(opts, cb) { +// $.checkState(this.credentials); +// $.shouldBeFunction(cb); - this._load({ - requiredAccess: access, - }, function(err, wcd) { - if (err) return cb(err); - var v = []; +// var self = this; - var myXPubKey = wcd.xPrivKey ? (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString() : ''; +// opts = opts || {}; +// var access = opts.access || 'full'; - _.each(WALLET_CRITICAL_DATA, function(k) { - var d; +// this._load(function(err, wcd) { +// if (err) return cb(err); +// var v = []; - if (access != 'full' && k === 'xPrivKey') { - v.push(null); - return; - } +// var myXPubKey = wcd.xPrivKey ? (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString() : ''; - // Skips own pub key IF priv key is exported - if (access == 'full' && k === 'publicKeyRing') { - d = _.without(wcd[k], myXPubKey); - } else { - d = wcd[k]; - } - v.push(d); - }); +// _.each(WALLET_CRITICAL_DATA, function(k) { +// var d; - if (access != 'full') { - v.push(wcd.copayerId); - v.push(wcd.roPrivKey); - if (access == 'readwrite') { - v.push(wcd.rwPrivKey); - } - } +// if (access != 'full' && k === 'xPrivKey') { +// v.push(null); +// return; +// } - return cb(null, JSON.stringify(v)); - }); -} +// // Skips own pub key IF priv key is exported +// if (access == 'full' && k === 'publicKeyRing') { +// d = _.without(wcd[k], myXPubKey); +// } else { +// d = wcd[k]; +// } +// v.push(d); +// }); +// if (access != 'full') { +// v.push(wcd.copayerId); +// v.push(wcd.roPrivKey); +// if (access == 'readwrite') { +// v.push(wcd.requestPrivKey); +// } +// } -API.prototype.import = function(str, cb) { - var self = this; +// return cb(null, JSON.stringify(v)); +// }); +// } - this.storage.load(function(err, wcd) { - if (wcd) - return cb('Storage already contains a wallet'); - wcd = {}; +// API.prototype.import = function(str, cb) { +// var self = this; - var inData = JSON.parse(str); - var i = 0; +// this.storage.load(function(err, wcd) { +// if (wcd) +// return cb('Storage already contains a wallet'); - _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { - wcd[k] = inData[i++]; - }); +// wcd = {}; - if (wcd.xPrivKey) { - var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey); - var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); - wcd.publicKeyRing.unshift(xPubKey); - wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey); - wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); - wcd.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); - } +// var inData = JSON.parse(str); +// var i = 0; - if (!wcd.publicKeyRing) - return cb('Invalid source wallet'); +// _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { +// wcd[k] = inData[i++]; +// }); - wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; +// if (wcd.xPrivKey) { +// var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey); +// var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); +// wcd.publicKeyRing.unshift(xPubKey); +// wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey); +// wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); +// wcd.requestPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); +// } - self.save(wcd, function(err) { - return cb(err, WalletUtils.accessFromData(wcd)); - }); - }); -}; +// if (!wcd.publicKeyRing) +// return cb('Invalid source wallet'); + +// wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; + +// self.save(wcd, function(err) { +// return cb(err, WalletUtils.accessFromData(wcd)); +// }); +// }); +// }; /** * */ API.prototype.parseTxProposals = function(txData, cb) { - var self = this; + $.checkState(this.credentials && this.credentials.isComplete()); - this._loadAndCheck({ - requiredAccess: 'readonly', - toComplete: txData.toComplete - }, function(err, wcd) { - if (err) return cb(err); + var self = this; - var txps = txData.txps; - _processTxps(txps, wcd.sharedEncryptingKey); + var txps = txData.txps; + _processTxps(txps, self.credentials.sharedEncryptingKey); - var fake = _.any(txps, function(txp) { - return (!Verifier.checkTxProposal(wcd, txp)); - }); + var fake = _.any(txps, function(txp) { + return (!Verifier.checkTxProposal(self.credentials, txp)); + }); - if (fake) - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); + if (fake) + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - return cb(null, txps); - }); + return cb(null, txps); }; @@ -725,42 +489,42 @@ API.prototype.parseTxProposals = function(txData, cb) { */ API.prototype.getTxProposals = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly' - }, function(err, wcd) { + var url = '/v1/txproposals/'; + self._doGetRequest(url, function(err, txps) { if (err) return cb(err); - var url = '/v1/txproposals/'; - self._doGetRequest(url, wcd, function(err, txps) { - if (err) return cb(err); - var rawTxps; - if (opts.getRawTxps) - rawTxps = JSON.parse(JSON.stringify(txps)); + var rawTxps; + if (opts.getRawTxps) { + rawTxps = JSON.parse(JSON.stringify(txps)); + } - _processTxps(txps, wcd.sharedEncryptingKey); + _processTxps(txps, self.credentials.sharedEncryptingKey); - var fake = _.any(txps, function(txp) { - return (!opts.doNotVerify && !Verifier.checkTxProposal(wcd, txp)); - }); + var fake = _.any(txps, function(txp) { + return (!opts.doNotVerify && !Verifier.checkTxProposal(self.credentials, txp)); + }); - if (fake) - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); + if (fake) + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - return cb(null, txps, rawTxps); - }); + // TODO: return a single arg + return cb(null, txps, rawTxps); }); }; -API.prototype._getSignaturesFor = function(txp, wcd) { +API.prototype._getSignaturesFor = function(txp) { + var self = this; //Derive proper key to sign, for each input var privs = [], derived = {}; var network = new Bitcore.Address(txp.toAddress).network.name; - var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey, network); + var xpriv = new Bitcore.HDPrivateKey(self.credentials.xPrivKey, network); _.each(txp.inputs, function(i) { if (!derived[i.path]) { @@ -790,121 +554,113 @@ API.prototype._getSignaturesFor = function(txp, wcd) { }; API.prototype.getSignatures = function(txp, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(txp.creatorId); + var self = this; - this._loadAndCheck({ - requiredAccess: 'full' - }, function(err, wcd) { - if (err) return cb(err); + if (!self.credentials.canSign()) + return cb('You do not have the required keys to sign transactions'); - if (!Verifier.checkTxProposal(wcd, txp)) { - return cb(new ServerCompromisedError('Transaction proposal is invalid')); - } + if (!Verifier.checkTxProposal(self.credentials, txp)) { + return cb(new ServerCompromisedError('Transaction proposal is invalid')); + } - return cb(null, self._getSignaturesFor(txp, wcd)); - }); + return cb(null, self._getSignaturesFor(txp)); }; -API.prototype.getEncryptedWalletData = function(cb) { - var self = this; +// API.prototype.getEncryptedWalletData = function(cb) { +// var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly' - }, function(err, wcd) { - if (err) return cb(err); - var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE)); - return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey))); - }); -}; +// this._loadAndCheck(function(err, wcd) { +// if (err) return cb(err); +// var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE)); +// return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey))); +// }); +// }; API.prototype.signTxProposal = function(txp, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(txp.creatorId); var self = this; - this._loadAndCheck({ - requiredAccess: txp.signatures ? 'readwrite' : 'full' - }, function(err, wcd) { - if (err) return cb(err); + if (!self.credentials.canSign() && !txp.signatures) + return cb(new Error('You do not have the required keys to sign transactions')); - if (!Verifier.checkTxProposal(wcd, txp)) { - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - } + if (!Verifier.checkTxProposal(self.credentials, txp)) { + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); + } - var signatures = txp.signatures || self._getSignaturesFor(txp, wcd); + var signatures = txp.signatures || self._getSignaturesFor(txp); - var url = '/v1/txproposals/' + txp.id + '/signatures/'; - var args = { - signatures: signatures - }; + var url = '/v1/txproposals/' + txp.id + '/signatures/'; + var args = { + signatures: signatures + }; - self._doPostRequest(url, args, wcd, cb); + self._doPostRequest(url, args, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); }); }; API.prototype.rejectTxProposal = function(txp, reason, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(cb); var self = this; - this._loadAndCheck({ - requiredAccess: 'readwrite' - }, - function(err, wcd) { - if (err) return cb(err); - - var url = '/v1/txproposals/' + txp.id + '/rejections/'; - var args = { - reason: _encryptMessage(reason, wcd.sharedEncryptingKey) || '', - }; - self._doPostRequest(url, args, wcd, cb); - }); + var url = '/v1/txproposals/' + txp.id + '/rejections/'; + var args = { + reason: _encryptMessage(reason, self.credentials.sharedEncryptingKey) || '', + }; + self._doPostRequest(url, args, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }); }; API.prototype.broadcastTxProposal = function(txp, cb) { - var self = this; + $.checkState(this.credentials && this.credentials.isComplete()); - this._loadAndCheck({ - requiredAccess: 'readwrite' - }, - function(err, wcd) { - if (err) return cb(err); + var self = this; - var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - self._doPostRequest(url, {}, wcd, cb); - }); + var url = '/v1/txproposals/' + txp.id + '/broadcast/'; + self._doPostRequest(url, {}, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }); }; API.prototype.removeTxProposal = function(txp, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({ - requiredAccess: 'readwrite' - }, - function(err, wcd) { - if (err) return cb(err); - var url = '/v1/txproposals/' + txp.id; - self._doRequest('delete', url, {}, wcd, cb); - }); + + var url = '/v1/txproposals/' + txp.id; + self._doRequest('delete', url, {}, function(err) { + if (err) return cb(err); + return cb(); + }); }; API.prototype.getTxHistory = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({}, function(err, wcd) { + var url = '/v1/txhistory/'; + self._doGetRequest(url, function(err, txs) { if (err) return cb(err); - var url = '/v1/txhistory/'; - self._doGetRequest(url, wcd, function(err, txs) { - if (err) return cb(err); - _processTxps(txs, wcd.sharedEncryptingKey); + _processTxps(txs, self.credentials.sharedEncryptingKey); - return cb(null, txs); - }); + return cb(null, txs); }); }; diff --git a/lib/client/credentials.js b/lib/client/credentials.js new file mode 100644 index 0000000..d59f18e --- /dev/null +++ b/lib/client/credentials.js @@ -0,0 +1,110 @@ +'use strict'; + +var $ = require('preconditions').singleton(); +var _ = require('lodash'); +var Bitcore = require('bitcore'); +var WalletUtils = require('../walletutils'); + +var FIELDS = [ + 'network', + 'xPrivKey', + 'xPubKey', + // 'roPrivKey', + 'requestPrivKey', + 'copayerId', + 'publicKeyRing', + 'walletId', + 'walletName', + 'm', + 'n', + 'walletPrivKey', + 'sharedEncryptingKey', + 'copayerName', +]; + +function Credentials() { + this.version = '1.0.0'; +}; + +Credentials.create = function(network) { + var x = new Credentials(); + + x.network = network; + x.xPrivKey = (new Bitcore.HDPrivateKey(network)).toString(); + x._expand(); + return x; +}; + +Credentials.fromExtendedPrivateKey = function(network, xPrivKey) { + var x = new Credentials(); + x.network = network; + x.xPrivKey = xPrivKey; + x._expand(); + return x; +}; + +Credentials.fromAirGapped = function(network, xPubKey, requestPrivKey) { + var x = new Credentials(); + x.network = network; + x.xPubKey = xPubKey; + x.requestPrivKey = requestPrivKey; + x._expand(); + return x; +}; + +Credentials.prototype._expand = function() { + $.checkState(this.xPrivKey || this.xPubKey); + + if (this.xPrivKey) { + var xPrivKey = new Bitcore.HDPrivateKey.fromString(this.xPrivKey); + this.xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); + // this.roPrivKey = xPrivKey.derive('m/1/0').privateKey.toString(); + this.requestPrivKey = xPrivKey.derive('m/1/1').privateKey.toString(); + } + this.copayerId = WalletUtils.xPubToCopayerId(this.xPubKey); +}; + +Credentials.fromObj = function(obj) { + var x = new Credentials(); + + _.each(FIELDS, function(k) { + x[k] = obj[k]; + }); + + return x; +}; + +Credentials.prototype.toObj = function() { + return this; +}; + +Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, walletPrivKey, copayerName) { + this.walletId = walletId; + this.walletName = walletName; + this.m = m; + this.n = n; + this.walletPrivKey = walletPrivKey; + this.sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); + this.copayerName = copayerName; + if (n == 1) { + this.addPublicKeyRing([this.xPubKey]); + } +}; + +Credentials.prototype.addPublicKeyRing = function(publicKeyRing) { + this.publicKeyRing = _.clone(publicKeyRing); +}; + +Credentials.prototype.canSign = function() { + return !!this.xPrivKey; +}; + +Credentials.prototype.isComplete = function() { + if (!this.walletId) return false; + if (!this.publicKeyRing || this.publicKeyRing.length != this.n) return false; + return true; +}; + + + +module.exports = Credentials; diff --git a/lib/client/filestorage.js b/lib/client/filestorage.js deleted file mode 100644 index 17769da..0000000 --- a/lib/client/filestorage.js +++ /dev/null @@ -1,33 +0,0 @@ - -var fs = require('fs') - -function FileStorage(opts) { - if (!opts.filename) { - throw new Error('Please set wallet filename'); - } - this.filename = opts.filename; - this.fs = opts.fs || fs; -}; - -FileStorage.prototype.getName = function() { - return this.filename; -}; - -FileStorage.prototype.save = function(data, cb) { - this.fs.writeFile(this.filename, JSON.stringify(data), cb); -}; - -FileStorage.prototype.load = function(cb) { - this.fs.readFile(this.filename, 'utf8', function(err,data) { - if (err) return cb(err); - try { - data = JSON.parse(data); - } catch (e) { - } - return cb(null, data); - }); -}; - - -module.exports = FileStorage; - diff --git a/lib/client/index.js b/lib/client/index.js index fd416f1..7e97815 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1,5 +1,3 @@ -//var client = ; - var client = module.exports = require('./api'); -client.FileStorage = require('./filestorage'); client.Verifier = require('./verifier'); +client.AirGapped = require('./airgapped'); diff --git a/lib/client/verifier.js b/lib/client/verifier.js index e720074..e9215d7 100644 --- a/lib/client/verifier.js +++ b/lib/client/verifier.js @@ -11,16 +11,15 @@ var WalletUtils = require('../walletutils') function Verifier(opts) {}; -Verifier.checkAddress = function(data, address) { - var local = WalletUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); +Verifier.checkAddress = function(credentials, address) { + var local = WalletUtils.deriveAddress(credentials.publicKeyRing, address.path, credentials.m, credentials.network); return (local.address == address.address && JSON.stringify(local.publicKeys) == JSON.stringify(address.publicKeys)); }; -Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { - $.checkArgument(walletPrivKey); - var walletPubKey = Bitcore.PrivateKey.fromString(walletPrivKey).toPublicKey().toString(); +Verifier.checkCopayers = function(credentials, copayers) { + var walletPubKey = Bitcore.PrivateKey.fromString(credentials.walletPrivKey).toPublicKey().toString(); - if (copayers.length != n) { + if (copayers.length != credentials.n) { log.error('Missing public keys in server response'); return false; } @@ -44,8 +43,7 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { if (error) return false; - var myXPubKey = new Bitcore.HDPublicKey(myXPrivKey).toString(); - if (!_.contains(_.pluck(copayers, 'xPubKey'), myXPubKey)) { + if (!_.contains(_.pluck(copayers, 'xPubKey'), credentials.xPubKey)) { log.error('Server response does not contains our public keys') return false; } @@ -53,10 +51,10 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { }; -Verifier.checkTxProposal = function(data, txp) { +Verifier.checkTxProposal = function(credentials, txp) { $.checkArgument(txp.creatorId); - var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) { + var creatorXPubKey = _.find(credentials.publicKeyRing, function(xPubKey) { if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true; }); @@ -71,7 +69,7 @@ Verifier.checkTxProposal = function(data, txp) { if (!WalletUtils.verifyMessage(hash, txp.proposalSignature, creatorSigningPubKey)) return false; - return Verifier.checkAddress(data, txp.changeAddress); + return Verifier.checkAddress(credentials, txp.changeAddress); }; module.exports = Verifier; diff --git a/lib/expressapp.js b/lib/expressapp.js index e1c4a63..726b1e6 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -24,7 +24,6 @@ var ExpressApp = function() {}; ExpressApp.start = function(opts) { opts = opts || {}; - WalletService.initialize(opts.WalletService); var app = express(); app.use(function(req, res, next) { @@ -104,13 +103,10 @@ ExpressApp.start = function(opts) { code: 'NOTAUTHORIZED' }), res, req); - var readOnly = req.method == 'GET'; - var auth = { copayerId: credentials.copayerId, message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body), signature: credentials.signature, - readOnly: readOnly, }; WalletService.getInstanceWithAuth(auth, function(err, server) { if (err) return returnError(err, res, req); diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 3c3f54f..6869500 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -11,7 +11,6 @@ var AddressManager = require('./addressmanager'); var Utils = require('../walletutils'); -var RO_SIGNING_PATH = "m/1/0"; var RW_SIGNING_PATH = "m/1/1"; function Copayer() { @@ -31,7 +30,6 @@ Copayer.create = function(opts) { x.id = Utils.xPubToCopayerId(x.xPubKey); x.name = opts.name; x.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently - x.roPubKey = x.getROPubKey(); x.rwPubKey = x.getRWPubKey(); x.addressManager = AddressManager.create({ copayerIndex: opts.copayerIndex @@ -63,10 +61,6 @@ Copayer.prototype.getPublicKey = function(path) { .toString(); }; -Copayer.prototype.getROPubKey = function() { - return this.getPublicKey(RO_SIGNING_PATH); -}; - Copayer.prototype.getRWPubKey = function() { return this.getPublicKey(RW_SIGNING_PATH); }; diff --git a/lib/model/txproposalaction.js b/lib/model/txproposalaction.js index 8e6dd31..03047c9 100644 --- a/lib/model/txproposalaction.js +++ b/lib/model/txproposalaction.js @@ -11,7 +11,7 @@ TxProposalAction.create = function(opts) { x.createdOn = Math.floor(Date.now() / 1000); x.copayerId = opts.copayerId; - x.type = opts.type || (opts.signatures ? 'accept' : 'reject'); + x.type = opts.type; x.signatures = opts.signatures; x.xpub = opts.xpub; x.comment = opts.comment; diff --git a/lib/server.js b/lib/server.js index aeb6905..ace2272 100644 --- a/lib/server.js +++ b/lib/server.js @@ -66,8 +66,7 @@ WalletService.getInstance = function() { * @param {Object} opts * @param {string} opts.copayerId - The copayer id making the request. * @param {string} opts.message - The contents of the request to be signed. - * @param {string} opts.signature - Signature of message to be verified using the copayer's roPubKey / rwPubKey - * @param {string} opts.readOnly - Signature of message to be verified using the copayer's roPubKey / rwPubKey + * @param {string} opts.signature - Signature of message to be verified using the copayer's rwPubKey */ WalletService.getInstanceWithAuth = function(opts, cb) { @@ -79,8 +78,7 @@ WalletService.getInstanceWithAuth = function(opts, cb) { if (err) return cb(err); if (!copayer) return cb(new ClientError('NOTAUTHORIZED', 'Copayer not found')); - var pubKey = opts.readOnly ? copayer.roPubKey : copayer.rwPubKey; - var isValid = server._verifySignature(opts.message, opts.signature, pubKey); + var isValid = server._verifySignature(opts.message, opts.signature, copayer.rwPubKey); if (!isValid) return cb(new ClientError('NOTAUTHORIZED', 'Invalid signature')); @@ -370,8 +368,7 @@ WalletService.prototype._getUtxos = function(cb) { // Get addresses for this wallet self.storage.fetchAddresses(self.walletId, function(err, addresses) { if (err) return cb(err); - if (addresses.length == 0) - return cb(null, []); + if (addresses.length == 0) return cb(null, []); var addressStrs = _.pluck(addresses, 'address'); var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance diff --git a/lib/walletutils.js b/lib/walletutils.js index 70a983e..487d275 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -37,7 +37,7 @@ WalletUtils.accessFromData = function(data) { if (data.xPrivKey) return 'full'; - if (data.rwPrivKey) + if (data.requestPrivKey) return 'readwrite'; if (data.roPrivKey) @@ -177,7 +177,7 @@ WalletUtils.encryptWallet = function(data, accessWithoutEncrytion, password) { // Fields to encrypt, given the NOPASSWD access level var fieldsEncryptByLevel = { none: _.keys(data), - readonly: ['xPrivKey', 'rwPrivKey', 'publicKeyRing' ], + readonly: ['xPrivKey', 'requestPrivKey', 'publicKeyRing'], readwrite: ['xPrivKey', ], full: [], }; @@ -187,7 +187,7 @@ WalletUtils.encryptWallet = function(data, accessWithoutEncrytion, password) { var toEncrypt = _.pick(data, fieldsEncrypt); var enc = sjcl.encrypt(password, JSON.stringify(toEncrypt), WalletUtils.sjclOpts); - + var ret = _.omit(data, fieldsEncrypt); ret.enc = enc; return ret; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 3a66e37..f5bc7f9 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -9,7 +9,7 @@ var memdown = require('memdown'); var async = require('async'); var request = require('supertest'); var Client = require('../../lib/client'); -var API = Client.API; +var AirGapped = Client.AirGapped; var Bitcore = require('bitcore'); var WalletUtils = require('../../lib/walletutils'); var ExpressApp = require('../../lib/expressapp'); @@ -41,48 +41,48 @@ helpers.getRequest = function(app) { helpers.createAndJoinWallet = function(clients, m, n, cb) { clients[0].createWallet('wallet name', 'creator', m, n, 'testnet', function(err, secret) { - if (err) return cb(err); - if (n == 1) return cb(); - - should.exist(secret); - async.each(_.range(n - 1), function(i, cb) { - clients[i + 1].joinWallet(secret, 'copayer ' + (i + 1), function(err, result) { + should.not.exist(err); + + if (n > 1) { + should.exist(secret); + } + + async.series([ + + function(next) { + async.each(_.range(1, n), function(i, cb) { + clients[i].joinWallet(secret, 'copayer ' + i, cb); + }, next); + }, + function(next) { + async.each(_.range(n), function(i, cb) { + clients[i].openWallet(cb); + }, next); + }, + ], + function(err) { should.not.exist(err); - return cb(err); - }); - }, function(err) { - if (err) return cb(err); - return cb(null, { - m: m, - n: n, - secret: secret, + return cb({ + m: m, + n: n, + secret: secret, + }); }); - }); }); }; - -var fsmock = {}; -var content = {}; -fsmock.readFile = function(name, enc, cb) { - if (!content || _.isEmpty(content[name])) - return cb('NOTFOUND'); - - return cb(null, content[name]); -}; -fsmock.writeFile = function(name, data, cb) { - content[name] = data; - return cb(); -}; -fsmock.reset = function() { - content = {}; -}; - -fsmock._get = function(name) { - return content[name]; -}; -fsmock._set = function(name, data) { - return content[name] = data; +helpers.tamperResponse = function(clients, method, url, args, tamper, cb) { + clients = [].concat(clients); + // Use first client to get a clean response from server + clients[0]._doRequest(method, url, args, function(err, result) { + should.not.exist(err); + tamper(result); + // Return tampered data for every client in the list + _.each(clients, function(client) { + client._doRequest = sinon.stub().withArgs(method, url).yields(null, result); + }); + return cb(); + }); }; @@ -133,7 +133,6 @@ describe('client API ', function() { var clients, app; beforeEach(function() { - clients = []; var db = levelup(memdown, { valueEncoding: 'json' }); @@ -148,25 +147,18 @@ describe('client API ', function() { disableLogs: true, }); // Generates 5 clients - _.each(_.range(5), function(i) { - var storage = new Client.FileStorage({ - filename: 'client' + i, - fs: fsmock, + clients = _.map(_.range(5), function(i) { + return new Client({ + request: helpers.getRequest(app), }); - var client = new Client({ - storage: storage, - }); - - client.request = helpers.getRequest(app); - clients.push(client); }); - fsmock.reset(); blockExplorerMock.reset(); }); describe('Server internals', function() { it('should allow cors', function(done) { - clients[0]._doRequest('options', '/', null, {}, function(err, x, headers) { + clients[0].credentials = {}; + clients[0]._doRequest('options', '/', {}, function(err, x, headers) { headers['access-control-allow-origin'].should.equal('*'); should.exist(headers['access-control-allow-methods']); should.exist(headers['access-control-allow-headers']); @@ -226,115 +218,9 @@ describe('client API ', function() { }); }); - describe('Storage Encryption', function() { - beforeEach(function() { - _.each(_.range(3), function(i) { - clients[i].on('needPassword', function(cb) { - return cb('1234#$@#%F,./.**'); - }); - clients[i].on('needNewPassword', function(cb) { - return cb('1234#$@#%F,./.**'); - }); - - }); - }); - - - it('full encryption roundtrip', function(done) { - clients[0].setNopasswdAccess('none'); - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - - // Load it - var wcd = JSON.parse(fsmock._get('client0')); - fsmock._set('client1', wcd); - clients[1].getBalance(function(err, bal0) { - should.not.exist(err); - done(); - }); - }); - }); - - it('should fail if wrong password', function(done) { - clients[0].setNopasswdAccess('none'); - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - - // Load it - var wcd = JSON.parse(fsmock._get('client0')); - fsmock._set('client4', wcd); - - clients[4].on('needPassword', function(cb) { - return cb('1'); - }); - - clients[4].getBalance(function(err, bal0) { - err.should.equal('NOTAUTH'); - done(); - }); - }); - }); - - - it('should encrypt everything', function(done) { - clients[0].setNopasswdAccess('none'); - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - var wcd = JSON.parse(fsmock._get('client0')); - _.keys(wcd).should.deep.equal(['enc']); - done(); - }); - }); - - it('should encrypt xpriv access', function(done) { - clients[0].setNopasswdAccess('readwrite'); - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - var wcd = JSON.parse(fsmock._get('client0')); - should.exist(wcd.enc); - should.not.exist(wcd.xpriv); - done(); - }); - }); - - it('should encrypt rwkey', function(done) { - clients[0].setNopasswdAccess('readonly'); - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - var wcd = JSON.parse(fsmock._get('client0')); - should.exist(wcd.enc); - should.not.exist(wcd.xpriv); - should.not.exist(wcd.rwPrivKey); - done(); - }); - }); - - - _.each(['full', 'readwrite', 'readonly', 'none'], function(k) { - it('full encryption roundtrip: type:' + k, function(done) { - clients[0].setNopasswdAccess(k); - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - - // Load it - var wcd = JSON.parse(fsmock._get('client0')); - fsmock._set('client1', wcd); - clients[1].getBalance(function(err, bal0) { - should.not.exist(err); - done(); - }); - }); - }); - }); - - it.skip('should not ask for password if not needed (readonly)', function(done) {}); - it.skip('should not ask for password if not needed (readwrite)', function(done) {}); - }); - describe('Wallet Creation', function() { it('should check balance in a 1-1 ', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].getBalance(function(err, x) { should.not.exist(err); done(); @@ -342,8 +228,7 @@ describe('client API ', function() { }); }); it('should be able to complete wallets in copayer that joined later', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 2, 3, function() { clients[0].getBalance(function(err, x) { should.not.exist(err); clients[1].getBalance(function(err, x) { @@ -358,8 +243,7 @@ describe('client API ', function() { }); it('should not allow to join a full wallet ', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 2, 2, function(w) { should.exist(w.secret); clients[4].joinWallet(w.secret, 'copayer', function(err, result) { err.code.should.contain('WFULL'); @@ -386,182 +270,56 @@ describe('client API ', function() { done(); }); }); - it('should reject wallets with bad signatures', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/wallets/'; - clients[0]._doGetRequest(url, data, function(err, x) { - - // Tamper data - x.wallet.copayers[0].xPubKey = x.wallet.copayers[1].xPubKey; - // Tamper response - clients[1]._doGetRequest = sinon.stub().yields(null, x); - - clients[1].getBalance(function(err, x) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + it('should reject wallets with bad signatures', function(done) { + // Do not complete clients[1] pkr + var openWalletStub = sinon.stub(clients[1], 'openWallet').yields(); + + helpers.createAndJoinWallet(clients, 2, 3, function() { + helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) { + status.wallet.copayers[0].xPubKey = status.wallet.copayers[1].xPubKey; + }, function() { + openWalletStub.restore(); + clients[1].openWallet(function(err, x) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); }); it('should reject wallets with missing signatures', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); - - // Get right response - var data = clients[0]._load({}, function(err, data) { - var url = '/v1/wallets/'; - clients[0]._doGetRequest(url, data, function(err, x) { - - // Tamper data - delete x.wallet.copayers[1].xPubKey; - - // Tamper response - clients[1]._doGetRequest = sinon.stub().yields(null, x); - - clients[1].getBalance(function(err, x) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - - - it('should reject wallets missing caller"s pubkey', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); - - // Get right response - var data = clients[0]._load({}, function(err, data) { - var url = '/v1/wallets/'; - clients[0]._doGetRequest(url, data, function(err, x) { - - // Tamper data. Replace caller's pubkey - x.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey; - // Add a correct signature - x.wallet.copayers[1].xPubKeySignature = WalletUtils.signMessage( - x.wallet.copayers[1].xPubKey, data.walletPrivKey), - - // Tamper response - clients[1]._doGetRequest = sinon.stub().yields(null, x); - - clients[1].getBalance(function(err, x) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - }); - - describe('Access control', function() { - it('should not be able to create address if not rwPubKey', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - - var data = JSON.parse(fsmock._get('client0')); - delete data.rwPrivKey; - fsmock._set('client0', JSON.stringify(data)); - data.rwPrivKey = null; - - // Overwrite client's API auth checks - clients[0]._processWcdAfterRead = function(rawData, xx, cb) { - return cb(null, rawData); - }; - - clients[0].createAddress(function(err, x0) { - err.code.should.equal('NOTAUTHORIZED'); - done(); - }); - }); - }); - it('should not be able to create address from a ro export', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - clients[0].export({ - access: 'readonly' - }, function(err, str) { - should.not.exist(err); - clients[1].import(str, function(err, wallet) { - should.not.exist(err); - - // Overwrite client's API auth checks - clients[1]._processWcdAfterRead = function(rawData, xx, cb) { - return cb(null, rawData); - }; - - clients[1].createAddress(function(err, x0) { - err.code.should.equal('NOTAUTHORIZED'); - clients[0].createAddress(function(err, x0) { - should.not.exist(err); - done(); - }); - }); - }); - }); - }); - }); - it('should be able to create address from a rw export', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); - clients[0].export({ - access: 'readwrite' - }, function(err, str) { - should.not.exist(err); - clients[1].import(str, function(err, wallet) { - should.not.exist(err); - clients[1].createAddress(function(err, x0) { - should.not.exist(err); - done(); - }); + // Do not complete clients[1] pkr + var openWalletStub = sinon.stub(clients[1], 'openWallet').yields(); + + helpers.createAndJoinWallet(clients, 2, 3, function() { + helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) { + delete status.wallet.copayers[1].xPubKey; + }, function() { + openWalletStub.restore(); + clients[1].openWallet(function(err, x) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); }); - it('should not be able to create tx proposals from a rw export', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { - should.not.exist(err); - clients[0].export({ - access: 'readwrite' - }, function(err, str) { - clients[1].import(str, function(err, wallet) { - should.not.exist(err); - clients[1].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - var opts = { - amount: 10000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[1].sendTxProposal(opts, function(err, x) { - should.not.exist(err); - - // Overwrite client's API auth checks - clients[1]._processWcdAfterRead = function(rawData, xx, cb) { - return cb(null, rawData); - }; - - clients[1].signTxProposal(x, function(err, tx) { - err.code.should.be.equal('BADSIGNATURES'); - clients[1].getTxProposals({}, function(err, txs) { - should.not.exist(err); - txs[0].status.should.equal('pending'); - done(); - }); - }); - }); - }); + it('should reject wallets missing callers pubkey', function(done) { + // Do not complete clients[1] pkr + var openWalletStub = sinon.stub(clients[1], 'openWallet').yields(); + + helpers.createAndJoinWallet(clients, 2, 3, function() { + helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) { + // Replace caller's pubkey + status.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey; + // Add a correct signature + status.wallet.copayers[1].xPubKeySignature = WalletUtils.signMessage(status.wallet.copayers[1].xPubKey, clients[0].credentials.walletPrivKey); + }, function() { + openWalletStub.restore(); + clients[1].openWallet(function(err, x) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); @@ -570,8 +328,7 @@ describe('client API ', function() { describe('Air gapped related flows', function() { it('should be able get Tx proposals from a file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 1, 1); @@ -601,8 +358,7 @@ describe('client API ', function() { }); }); it('should detect fakes from Tx proposals file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 1, 1); @@ -634,114 +390,106 @@ describe('client API ', function() { }); }); - it('should create from proxy from airgapped', function(done) { - - var airgapped = clients[0]; - var proxy = clients[1]; + it('should create wallet in proxy from airgapped', function(done) { + var airgapped = new AirGapped({ + network: 'testnet' + }); + var seed = airgapped.getSeed(); - airgapped.generateKey('testnet', function(err) { + var proxy = new Client({ + request: helpers.getRequest(app), + }); + proxy.seedFromAirGapped(seed); + should.not.exist(proxy.credentials.xPrivKey); + proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { should.not.exist(err); - airgapped.export({ - access: 'readwrite' - }, function(err, str) { - proxy.import(str, function(err) { - should.not.exist(err); - proxy.createWallet('1', '2', 1, 1, 'testnet', - function(err) { - should.not.exist(err); - // should keep cpub - var c0 = JSON.parse(fsmock._get('client0')); - var c1 = JSON.parse(fsmock._get('client1')); - _.each(['copayerId', 'network', 'publicKeyRing', - 'roPrivKey', 'rwPrivKey' - ], function(k) { - c0[k].should.deep.equal(c1[k]); - }); - done(); - }); - }); + proxy.getStatus(function(err, status) { + should.not.exist(err); + status.wallet.name.should.equal('wallet name'); + done(); }); }); }); - it('should join from proxy from airgapped', function(done) { + it.skip('should be able to sign from airgapped client and broadcast from proxy', function(done) { + var airgapped = new AirGapped({ + network: 'testnet' + }); + var seed = airgapped.getSeed(); - var airgapped = clients[0]; - var proxy = clients[1]; - var other = clients[2]; // Other copayer + var proxy = new Client({ + request: helpers.getRequest(app), + }); + proxy.seedFromAirGapped(seed); - airgapped.generateKey('testnet', function(err) { - should.not.exist(err); - airgapped.export({ - access: 'readwrite' - }, function(err, str) { - proxy.import(str, function(err) { - should.not.exist(err); + async.waterfall([ - other.createWallet('1', '2', 1, 2, 'testnet', function(err, secret) { + function(next) { + proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { should.not.exist(err); - proxy.joinWallet(secret, 'john', function(err) { + proxy.createAddress(function(err, address) { should.not.exist(err); - // should keep cpub - var c0 = JSON.parse(fsmock._get('client0')); - var c1 = JSON.parse(fsmock._get('client1')); - _.each(['copayerId', 'network', 'publicKeyRing', - 'roPrivKey', 'rwPrivKey' - ], function(k) { - c0[k].should.deep.equal(c1[k]); - }); - done(); - }) + should.exist(address.address); + blockExplorerMock.setUtxo(address, 1, 1); + var opts = { + amount: 1200000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hello 1-1', + }; + proxy.sendTxProposal(opts, next); + }); }); - }); - }); - }); - }); - - it('should be able export signatures and sign later from a ro client', - function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { - should.not.exist(err); - clients[0].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - blockExplorerMock.setUtxo(x0, 1, 2); - var opts = { - amount: 150000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[0].sendTxProposal(opts, function(err, txp) { - should.not.exist(err); - clients[0].getSignatures(txp, function(err, signatures) { - should.not.exist(err); - signatures.length.should.equal(txp.inputs.length); - signatures[0].length.should.above(62 * 2); - - txp.signatures = signatures; - - // Make client RO - var data = JSON.parse(fsmock._get('client0')); - delete data.xPrivKey; - fsmock._set('client0', JSON.stringify(data)); - - clients[0].signTxProposal(txp, function(err, txp) { + }, + function(txp, next) { + should.exist(txp); + proxy.signTxProposal(txp, function(err, txp) { + should.exist(err); + should.not.exist(txp); + err.message.should.equal('You do not have the required keys to sign transactions'); + next(null, txp); + }); + }, + function(txp, next) { + proxy.getTxProposals({ + getRawTxps: true + }, next); + }, + function(txps, rawTxps, next) { + airgapped.signTxProposals(rawTxps, next); + }, + function(signatures, next) { + proxy.getTxProposals({}, function(err, txps) { + _.each(txps, function(txp, i) { + txp.signatures = signatures[i]; + }); + async.each(txps, function(txp, cb) { + proxy.signTxProposal(txp, function(err, txp) { should.not.exist(err); - txp.status.should.equal('accepted'); - done(); + proxy.broadcastTxProposal(txp, function(err, txp) { + should.not.exist(err); + txp.status.should.equal('broadcasted'); + should.exist(txp.txid); + cb(); + }); }); + }, function(err) { + next(err); }); }); - }); - }); - }); + }, + ], + function(err) { + should.not.exist(err); + done(); + } + ); + }); }); describe('Address Creation', function() { it('should be able to create address in all copayers in a 2-3 wallet', function(done) { this.timeout(5000); - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 2, 3, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -758,8 +506,8 @@ describe('client API ', function() { }); }); it('should see balance on address created by others', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { - should.not.exist(err); + this.timeout(5000); + helpers.createAndJoinWallet(clients, 2, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -779,102 +527,77 @@ describe('client API ', function() { }); }); it('should detect fake addresses', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/addresses/'; - clients[0]._doPostRequest(url, {}, data, function(err, address) { - - // Tamper data - address.address = '2N86pNEpREGpwZyHVC5vrNUCbF9nM1Geh4K'; - - // Tamper response - clients[1]._doPostRequest = sinon.stub().yields(null, address); - - // Grab real response - clients[1].createAddress(function(err, x0) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - it('should detect fake public keys', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/addresses/'; - clients[0]._doPostRequest(url, {}, data, function(err, address) { - - // Tamper data - address.publicKeys = ['0322defe0c3eb9fcd8bc01878e6dbca7a6846880908d214b50a752445040cc5c54', - '02bf3aadc17131ca8144829fa1883c1ac0a8839067af4bca47a90ccae63d0d8037' - ]; - - // Tamper response - clients[1]._doPostRequest = sinon.stub().yields(null, address); - - // Grab real response - clients[1].createAddress(function(err, x0) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - }); - - describe('Wallet Backups and Mobility', function() { - - it('round trip #import #export', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { - should.not.exist(err); - clients[1].export({}, function(err, str) { - should.not.exist(err); - var original = JSON.parse(fsmock._get('client1')); - clients[2].import(str, function(err, wallet) { - should.not.exist(err); - var clone = JSON.parse(fsmock._get('client2')); - delete original.walletPrivKey; // no need to persist it. - clone.should.deep.equal(original); + helpers.createAndJoinWallet(clients, 1, 1, function() { + helpers.tamperResponse(clients[0], 'post', '/v1/addresses/', {}, function(address) { + address.address = '2N86pNEpREGpwZyHVC5vrNUCbF9nM1Geh4K'; + }, function() { + clients[0].createAddress(function(err, x0) { + err.code.should.contain('SERVERCOMPROMISED'); done(); }); - }); }); }); - it('should recreate a wallet, create addresses and receive money', function(done) { - var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; - clients[0].import(backup, function(err, wallet) { - should.not.exist(err); - clients[0].reCreateWallet('pepe', function(err, wallet) { - should.not.exist(err); - + it('should detect fake public keys', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { + helpers.tamperResponse(clients[0], 'post', '/v1/addresses/', {}, function(address) { + address.publicKeys = [ + '0322defe0c3eb9fcd8bc01878e6dbca7a6846880908d214b50a752445040cc5c54', + '02bf3aadc17131ca8144829fa1883c1ac0a8839067af4bca47a90ccae63d0d8037' + ]; + }, function() { clients[0].createAddress(function(err, x0) { - should.not.exist(err); - should.exist(x0.address); - blockExplorerMock.setUtxo(x0, 10, 2); - clients[0].getBalance(function(err, bal0) { - should.not.exist(err); - bal0.totalAmount.should.equal(10 * 1e8); - bal0.lockedAmount.should.equal(0); - done(); - }); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); }); }); + // describe.skip('Wallet Backups and Mobility', function() { + + // it('round trip #import #export', function(done) { + // helpers.createAndJoinWallet(clients, 2, 2, function(w) { + // clients[1].export({}, function(err, str) { + // should.not.exist(err); + // var original = JSON.parse(fsmock._get('client1')); + // clients[2].import(str, function(err, wallet) { + // should.not.exist(err); + // var clone = JSON.parse(fsmock._get('client2')); + // delete original.walletPrivKey; // no need to persist it. + // clone.should.deep.equal(original); + // done(); + // }); + + // }); + // }); + // }); + // it('should recreate a wallet, create addresses and receive money', function(done) { + // var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; + // clients[0].import(backup, function(err, wallet) { + // should.not.exist(err); + // clients[0].reCreateWallet('pepe', function(err, wallet) { + // should.not.exist(err); + + // clients[0].createAddress(function(err, x0) { + // should.not.exist(err); + // should.exist(x0.address); + // blockExplorerMock.setUtxo(x0, 10, 2); + // clients[0].getBalance(function(err, bal0) { + // should.not.exist(err); + // bal0.totalAmount.should.equal(10 * 1e8); + // bal0.lockedAmount.should.equal(0); + // done(); + // }); + // }); + // }); + // }); + // }); + // }); describe('Transaction Proposals Creation and Locked funds', function() { it('Should lock and release funds through rejection', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -905,7 +628,7 @@ describe('client API ', function() { }); }); it('Should lock and release funds through removal', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -936,7 +659,7 @@ describe('client API ', function() { }); }); it('Should keep message and refusal texts', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -962,7 +685,7 @@ describe('client API ', function() { }); }); it('Should encrypt proposal message', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -982,7 +705,7 @@ describe('client API ', function() { }); }); it('Should encrypt proposal refusal comment', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1004,9 +727,7 @@ describe('client API ', function() { }); }); it('should detect fake tx proposals (wrong signature)', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1018,34 +739,22 @@ describe('client API ', function() { clients[0].sendTxProposal(opts, function(err, x) { should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/txproposals/'; - clients[0]._doGetRequest(url, data, function(err, txps) { - - // Tamper data - txps[0].proposalSignature = '304402206e4a1db06e00068582d3be41cfc795dcf702451c132581e661e7241ef34ca19202203e17598b4764913309897d56446b51bc1dcd41a25d90fdb5f87a6b58fe3a6920'; - - // Tamper response - clients[0]._doGetRequest = sinon.stub().yields(null, txps); - - // Grab real response - clients[0].getTxProposals({}, function(err, txps) { - should.exist(err); - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + helpers.tamperResponse(clients[0], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].proposalSignature = '304402206e4a1db06e00068582d3be41cfc795dcf702451c132581e661e7241ef34ca19202203e17598b4764913309897d56446b51bc1dcd41a25d90fdb5f87a6b58fe3a6920'; + }, function() { + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); }); }); }); - it('should detect fake tx proposals (tampered amount)', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); + it('should detect fake tx proposals (tampered amount)', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1057,24 +766,13 @@ describe('client API ', function() { clients[0].sendTxProposal(opts, function(err, x) { should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/txproposals/'; - clients[0]._doGetRequest(url, data, function(err, txps) { - - // Tamper data - txps[0].amount = 100000; - - // Tamper response - clients[0]._doGetRequest = sinon.stub().yields(null, txps); - - // Grab real response - clients[0].getTxProposals({}, function(err, txps) { - should.exist(err); - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + helpers.tamperResponse(clients[0], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].amount = 100000; + }, function() { + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); @@ -1082,9 +780,7 @@ describe('client API ', function() { }); }); it('should detect fake tx proposals (change address not it wallet)', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1096,23 +792,13 @@ describe('client API ', function() { clients[0].sendTxProposal(opts, function(err, x) { should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/txproposals/'; - clients[0]._doGetRequest(url, data, function(err, txps) { - // Tamper data - txps[0].changeAddress.address = 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5'; - - // Tamper response - clients[0]._doGetRequest = sinon.stub().yields(null, txps); - - // Grab real response - clients[0].getTxProposals({}, function(err, txps) { - should.exist(err); - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + helpers.tamperResponse(clients[0], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].changeAddress.address = 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5'; + }, function() { + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); @@ -1120,8 +806,7 @@ describe('client API ', function() { }); }); it('Should return only main addresses (case 1)', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 1, 1); @@ -1142,8 +827,7 @@ describe('client API ', function() { }); }); it('Should return only main addresses (case 2)', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); clients[0].createAddress(function(err, x0) { @@ -1164,7 +848,7 @@ describe('client API ', function() { describe('Transactions Signatures and Rejection', function() { this.timeout(5000); it('Send and broadcast in 1-1 wallet', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1194,7 +878,7 @@ describe('client API ', function() { }); }); it('Send and broadcast in 2-3 wallet', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1240,7 +924,7 @@ describe('client API ', function() { }); it('Send, reject, 2 signs and broadcast in 2-3 wallet', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1277,7 +961,7 @@ describe('client API ', function() { }); it('Send, reject in 3-4 wallet', function(done) { - helpers.createAndJoinWallet(clients, 3, 4, function(err, w) { + helpers.createAndJoinWallet(clients, 3, 4, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1312,7 +996,7 @@ describe('client API ', function() { }); it('Should not allow to reject or sign twice', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1352,7 +1036,7 @@ describe('client API ', function() { describe('Transaction history', function() { it('should get transaction history', function(done) { blockExplorerMock.setHistory(TestData.history); - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1367,7 +1051,7 @@ describe('client API ', function() { }); it('should get empty transaction history when there are no addresses', function(done) { blockExplorerMock.setHistory(TestData.history); - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].getTxHistory({}, function(err, txs) { should.not.exist(err); should.exist(txs); From 7309d42711dc48d10aa606a1a9ea57787383f746 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sat, 28 Feb 2015 23:55:23 -0300 Subject: [PATCH 02/13] signing from airgapped client --- lib/client/airgapped.js | 12 +- lib/client/api.js | 53 +-- lib/client/verifier.js | 2 + lib/walletutils.js | 65 ++-- test/integration/{clientApi.js => client.js} | 364 +++++++++---------- 5 files changed, 214 insertions(+), 282 deletions(-) rename test/integration/{clientApi.js => client.js} (94%) diff --git a/lib/client/airgapped.js b/lib/client/airgapped.js index 14408fc..3e067c5 100644 --- a/lib/client/airgapped.js +++ b/lib/client/airgapped.js @@ -37,10 +37,14 @@ AirGapped.prototype.getSeed = function() { }; }; -AirGapped.prototype.signTxProposals = function(txps, cb) { - return cb(null, _.map(txps, function(txp) { - return {}; - })); +AirGapped.prototype.signTxProposal = function(txp) { + var self = this; + + // TODO: complete credentials + if (!Verifier.checkTxProposal(self.credentials, txp)) { + throw new Error('Fake transaction proposal'); + } + return WalletUtils.signTxp(txp, self.credentials.xPrivKey); }; module.exports = AirGapped; diff --git a/lib/client/api.js b/lib/client/api.js index 081b833..f38c637 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -516,43 +516,6 @@ API.prototype.getTxProposals = function(opts, cb) { }); }; -API.prototype._getSignaturesFor = function(txp) { - var self = this; - - //Derive proper key to sign, for each input - var privs = [], - derived = {}; - - var network = new Bitcore.Address(txp.toAddress).network.name; - var xpriv = new Bitcore.HDPrivateKey(self.credentials.xPrivKey, network); - - _.each(txp.inputs, function(i) { - if (!derived[i.path]) { - derived[i.path] = xpriv.derive(i.path).privateKey; - privs.push(derived[i.path]); - } - }); - - var t = new Bitcore.Transaction(); - - _.each(txp.inputs, function(i) { - t.from(i, i.publicKeys, txp.requiredSignatures); - }); - - t.to(txp.toAddress, txp.amount) - .change(txp.changeAddress.address); - - var signatures = _.map(privs, function(priv, i) { - return t.getSignatures(priv); - }); - - signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { - return s.signature.toDER().toString('hex'); - }); - - return signatures; -}; - API.prototype.getSignatures = function(txp, cb) { $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(txp.creatorId); @@ -566,21 +529,9 @@ API.prototype.getSignatures = function(txp, cb) { return cb(new ServerCompromisedError('Transaction proposal is invalid')); } - return cb(null, self._getSignaturesFor(txp)); + return cb(null, WalletUtils.signTxp(txp, self.credentials.xPrivKey)); }; -// API.prototype.getEncryptedWalletData = function(cb) { -// var self = this; - -// this._loadAndCheck(function(err, wcd) { -// if (err) return cb(err); -// var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE)); -// return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey))); -// }); -// }; - - - API.prototype.signTxProposal = function(txp, cb) { $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(txp.creatorId); @@ -594,7 +545,7 @@ API.prototype.signTxProposal = function(txp, cb) { return cb(new ServerCompromisedError('Server sent fake transaction proposal')); } - var signatures = txp.signatures || self._getSignaturesFor(txp); + var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey); var url = '/v1/txproposals/' + txp.id + '/signatures/'; var args = { diff --git a/lib/client/verifier.js b/lib/client/verifier.js index e9215d7..84f1518 100644 --- a/lib/client/verifier.js +++ b/lib/client/verifier.js @@ -12,6 +12,7 @@ var WalletUtils = require('../walletutils') function Verifier(opts) {}; Verifier.checkAddress = function(credentials, address) { + $.checkState(credentials.isComplete()); var local = WalletUtils.deriveAddress(credentials.publicKeyRing, address.path, credentials.m, credentials.network); return (local.address == address.address && JSON.stringify(local.publicKeys) == JSON.stringify(address.publicKeys)); }; @@ -53,6 +54,7 @@ Verifier.checkCopayers = function(credentials, copayers) { Verifier.checkTxProposal = function(credentials, txp) { $.checkArgument(txp.creatorId); + $.checkState(credentials.isComplete()); var creatorXPubKey = _.find(credentials.publicKeyRing, function(xPubKey) { if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true; diff --git a/lib/walletutils.js b/lib/walletutils.js index 487d275..acf5397 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -33,33 +33,6 @@ WalletUtils.signMessage = function(text, privKey) { }; -WalletUtils.accessFromData = function(data) { - if (data.xPrivKey) - return 'full'; - - if (data.requestPrivKey) - return 'readwrite'; - - if (data.roPrivKey) - return 'readonly'; - - return 'none'; -}; - -WalletUtils.accessNameToLevel = function(name) { - - if (name === 'full') - return 30; - if (name === 'readwrite') - return 20; - if (name === 'readonly') - return 10; - if (name === 'none') - return 0; - throw new Error('Bad access name:' + name); -}; - - WalletUtils.verifyMessage = function(text, signature, pubKey) { $.checkArgument(text); $.checkArgument(pubKey); @@ -194,4 +167,42 @@ WalletUtils.encryptWallet = function(data, accessWithoutEncrytion, password) { }; + +WalletUtils.signTxp = function(txp, xPrivKey) { + var self = this; + + //Derive proper key to sign, for each input + var privs = [], + derived = {}; + + var network = new Bitcore.Address(txp.toAddress).network.name; + var xpriv = new Bitcore.HDPrivateKey(xPrivKey, network); + + _.each(txp.inputs, function(i) { + if (!derived[i.path]) { + derived[i.path] = xpriv.derive(i.path).privateKey; + privs.push(derived[i.path]); + } + }); + + var t = new Bitcore.Transaction(); + + _.each(txp.inputs, function(i) { + t.from(i, i.publicKeys, txp.requiredSignatures); + }); + + t.to(txp.toAddress, txp.amount) + .change(txp.changeAddress.address); + + var signatures = _.map(privs, function(priv, i) { + return t.getSignatures(priv); + }); + + signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { + return s.signature.toDER().toString('hex'); + }); + + return signatures; +}; + module.exports = WalletUtils; diff --git a/test/integration/clientApi.js b/test/integration/client.js similarity index 94% rename from test/integration/clientApi.js rename to test/integration/client.js index f5bc7f9..8df6f1b 100644 --- a/test/integration/clientApi.js +++ b/test/integration/client.js @@ -326,166 +326,6 @@ describe('client API ', function() { }); }); - describe('Air gapped related flows', function() { - it('should be able get Tx proposals from a file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(w) { - clients[0].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - var opts = { - amount: 10000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[1].sendTxProposal(opts, function(err, x) { - should.not.exist(err); - clients[1].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - clients[0].parseTxProposals({ - txps: rawTxps - }, function(err, txs2) { - should.not.exist(err); - txs[0].should.deep.equal(txs2[0]); - done(); - }); - - }); - }); - }); - }); - }); - it('should detect fakes from Tx proposals file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(w) { - clients[0].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - var opts = { - amount: 10000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[1].sendTxProposal(opts, function(err, x) { - should.not.exist(err); - clients[1].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - //Tamper - rawTxps[0].amount++; - - clients[0].parseTxProposals({ - txps: rawTxps - }, function(err, txs2) { - err.code.should.equal('SERVERCOMPROMISED'); - done(); - }); - - }); - }); - }); - }); - }); - - it('should create wallet in proxy from airgapped', function(done) { - var airgapped = new AirGapped({ - network: 'testnet' - }); - var seed = airgapped.getSeed(); - - var proxy = new Client({ - request: helpers.getRequest(app), - }); - proxy.seedFromAirGapped(seed); - should.not.exist(proxy.credentials.xPrivKey); - proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { - should.not.exist(err); - proxy.getStatus(function(err, status) { - should.not.exist(err); - status.wallet.name.should.equal('wallet name'); - done(); - }); - }); - }); - - it.skip('should be able to sign from airgapped client and broadcast from proxy', function(done) { - var airgapped = new AirGapped({ - network: 'testnet' - }); - var seed = airgapped.getSeed(); - - var proxy = new Client({ - request: helpers.getRequest(app), - }); - proxy.seedFromAirGapped(seed); - - async.waterfall([ - - function(next) { - proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { - should.not.exist(err); - proxy.createAddress(function(err, address) { - should.not.exist(err); - should.exist(address.address); - blockExplorerMock.setUtxo(address, 1, 1); - var opts = { - amount: 1200000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - proxy.sendTxProposal(opts, next); - }); - }); - }, - function(txp, next) { - should.exist(txp); - proxy.signTxProposal(txp, function(err, txp) { - should.exist(err); - should.not.exist(txp); - err.message.should.equal('You do not have the required keys to sign transactions'); - next(null, txp); - }); - }, - function(txp, next) { - proxy.getTxProposals({ - getRawTxps: true - }, next); - }, - function(txps, rawTxps, next) { - airgapped.signTxProposals(rawTxps, next); - }, - function(signatures, next) { - proxy.getTxProposals({}, function(err, txps) { - _.each(txps, function(txp, i) { - txp.signatures = signatures[i]; - }); - async.each(txps, function(txp, cb) { - proxy.signTxProposal(txp, function(err, txp) { - should.not.exist(err); - proxy.broadcastTxProposal(txp, function(err, txp) { - should.not.exist(err); - txp.status.should.equal('broadcasted'); - should.exist(txp.txid); - cb(); - }); - }); - }, function(err) { - next(err); - }); - }); - }, - ], - function(err) { - should.not.exist(err); - done(); - } - ); - }); - }); - describe('Address Creation', function() { it('should be able to create address in all copayers in a 2-3 wallet', function(done) { this.timeout(5000); @@ -555,46 +395,6 @@ describe('client API ', function() { }); }); - // describe.skip('Wallet Backups and Mobility', function() { - - // it('round trip #import #export', function(done) { - // helpers.createAndJoinWallet(clients, 2, 2, function(w) { - // clients[1].export({}, function(err, str) { - // should.not.exist(err); - // var original = JSON.parse(fsmock._get('client1')); - // clients[2].import(str, function(err, wallet) { - // should.not.exist(err); - // var clone = JSON.parse(fsmock._get('client2')); - // delete original.walletPrivKey; // no need to persist it. - // clone.should.deep.equal(original); - // done(); - // }); - - // }); - // }); - // }); - // it('should recreate a wallet, create addresses and receive money', function(done) { - // var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; - // clients[0].import(backup, function(err, wallet) { - // should.not.exist(err); - // clients[0].reCreateWallet('pepe', function(err, wallet) { - // should.not.exist(err); - - // clients[0].createAddress(function(err, x0) { - // should.not.exist(err); - // should.exist(x0.address); - // blockExplorerMock.setUtxo(x0, 10, 2); - // clients[0].getBalance(function(err, bal0) { - // should.not.exist(err); - // bal0.totalAmount.should.equal(10 * 1e8); - // bal0.lockedAmount.should.equal(0); - // done(); - // }); - // }); - // }); - // }); - // }); - // }); describe('Transaction Proposals Creation and Locked funds', function() { it('Should lock and release funds through rejection', function(done) { helpers.createAndJoinWallet(clients, 2, 2, function(w) { @@ -1063,4 +863,168 @@ describe('client API ', function() { it.skip('should get transaction history decorated with proposal', function(done) {}); it.skip('should get paginated transaction history', function(done) {}); }); + + describe('Air gapped related flows', function() { + it('should be able get Tx proposals from a file', function(done) { + helpers.createAndJoinWallet(clients, 1, 2, function(w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 1, 1); + var opts = { + amount: 10000000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hello 1-1', + }; + clients[1].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + clients[1].getTxProposals({ + getRawTxps: true + }, function(err, txs, rawTxps) { + should.not.exist(err); + + clients[0].parseTxProposals({ + txps: rawTxps + }, function(err, txs2) { + should.not.exist(err); + txs[0].should.deep.equal(txs2[0]); + done(); + }); + + }); + }); + }); + }); + }); + it('should detect fakes from Tx proposals file', function(done) { + helpers.createAndJoinWallet(clients, 1, 2, function(w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 1, 1); + var opts = { + amount: 10000000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hello 1-1', + }; + clients[1].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + clients[1].getTxProposals({ + getRawTxps: true + }, function(err, txs, rawTxps) { + should.not.exist(err); + + //Tamper + rawTxps[0].amount++; + + clients[0].parseTxProposals({ + txps: rawTxps + }, function(err, txs2) { + err.code.should.equal('SERVERCOMPROMISED'); + done(); + }); + + }); + }); + }); + }); + }); + + it('should create wallet in proxy from airgapped', function(done) { + var airgapped = new AirGapped({ + network: 'testnet' + }); + var seed = airgapped.getSeed(); + + var proxy = new Client({ + request: helpers.getRequest(app), + }); + proxy.seedFromAirGapped(seed); + should.not.exist(proxy.credentials.xPrivKey); + proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { + should.not.exist(err); + proxy.getStatus(function(err, status) { + should.not.exist(err); + status.wallet.name.should.equal('wallet name'); + done(); + }); + }); + }); + + it.skip('should be able to sign from airgapped client and broadcast from proxy', function(done) { + var airgapped = new AirGapped({ + network: 'testnet' + }); + var seed = airgapped.getSeed(); + + var proxy = new Client({ + request: helpers.getRequest(app), + }); + proxy.seedFromAirGapped(seed); + + async.waterfall([ + + function(next) { + proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { + should.not.exist(err); + proxy.createAddress(function(err, address) { + should.not.exist(err); + should.exist(address.address); + blockExplorerMock.setUtxo(address, 1, 1); + var opts = { + amount: 1200000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hello 1-1', + }; + proxy.sendTxProposal(opts, next); + }); + }); + }, + function(txp, next) { + should.exist(txp); + proxy.signTxProposal(txp, function(err, txp) { + should.exist(err); + should.not.exist(txp); + err.message.should.equal('You do not have the required keys to sign transactions'); + next(null, txp); + }); + }, + function(txp, next) { + proxy.getTxProposals({ + getRawTxps: true + }, next); + }, + function(txps, rawTxps, next) { + var signatures = airgapped.signTxProposal(rawTxps[0]); + next(null, signatures); + }, + function(signatures, next) { + proxy.getTxProposals({}, function(err, txps) { + should.not.exist(err); + var txp = txps[0]; + txp.signatures = signatures; + async.each(txps, function(txp, cb) { + proxy.signTxProposal(txp, function(err, txp) { + should.not.exist(err); + proxy.broadcastTxProposal(txp, function(err, txp) { + should.not.exist(err); + txp.status.should.equal('broadcasted'); + should.exist(txp.txid); + cb(); + }); + }); + }, function(err) { + next(err); + }); + }); + }, + ], + function(err) { + should.not.exist(err); + done(); + } + ); + }); + it.skip('should be able to detect tampered pkr when signing on airgapped client', function(done) {}); + it.skip('should be able to detect tampered proposal when signing on airgapped client', function(done) {}); + it.skip('should be able to detect tampered change address when signing on airgapped client', function(done) {}); + }); }); From cabdb35cb122669cccaea4a64daf918b8166e3d3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 00:31:42 -0300 Subject: [PATCH 03/13] add params needed to check proposal on airgapped client --- lib/client/airgapped.js | 6 +++++- lib/client/credentials.js | 4 +--- lib/model/copayer.js | 11 ++++------- lib/server.js | 8 ++++---- lib/storage.js | 3 +-- test/integration/client.js | 9 +++++++-- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/client/airgapped.js b/lib/client/airgapped.js index 3e067c5..283ec6c 100644 --- a/lib/client/airgapped.js +++ b/lib/client/airgapped.js @@ -37,10 +37,14 @@ AirGapped.prototype.getSeed = function() { }; }; -AirGapped.prototype.signTxProposal = function(txp) { +AirGapped.prototype.signTxProposal = function(txp, pkr, m, n) { var self = this; // TODO: complete credentials + self.credentials.m = m; + self.credentials.n = n; + self.credentials.addPublicKeyRing(pkr); + if (!Verifier.checkTxProposal(self.credentials, txp)) { throw new Error('Fake transaction proposal'); } diff --git a/lib/client/credentials.js b/lib/client/credentials.js index d59f18e..5af6a76 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -9,7 +9,6 @@ var FIELDS = [ 'network', 'xPrivKey', 'xPubKey', - // 'roPrivKey', 'requestPrivKey', 'copayerId', 'publicKeyRing', @@ -58,7 +57,6 @@ Credentials.prototype._expand = function() { if (this.xPrivKey) { var xPrivKey = new Bitcore.HDPrivateKey.fromString(this.xPrivKey); this.xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); - // this.roPrivKey = xPrivKey.derive('m/1/0').privateKey.toString(); this.requestPrivKey = xPrivKey.derive('m/1/1').privateKey.toString(); } this.copayerId = WalletUtils.xPubToCopayerId(this.xPubKey); @@ -100,7 +98,7 @@ Credentials.prototype.canSign = function() { }; Credentials.prototype.isComplete = function() { - if (!this.walletId) return false; + if (!this.m || !this.n) return false; if (!this.publicKeyRing || this.publicKeyRing.length != this.n) return false; return true; }; diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 6869500..04a84cb 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -11,8 +11,6 @@ var AddressManager = require('./addressmanager'); var Utils = require('../walletutils'); -var RW_SIGNING_PATH = "m/1/1"; - function Copayer() { this.version = '1.0.0'; }; @@ -30,7 +28,7 @@ Copayer.create = function(opts) { x.id = Utils.xPubToCopayerId(x.xPubKey); x.name = opts.name; x.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently - x.rwPubKey = x.getRWPubKey(); + x.requestPubKey = x.getRequestPubKey(); x.addressManager = AddressManager.create({ copayerIndex: opts.copayerIndex }); @@ -46,8 +44,7 @@ Copayer.fromObj = function(obj) { x.name = obj.name; x.xPubKey = obj.xPubKey; x.xPubKeySignature = obj.xPubKeySignature; - x.roPubKey = obj.roPubKey; - x.rwPubKey = obj.rwPubKey; + x.requestPubKey = obj.requestPubKey; x.addressManager = AddressManager.fromObj(obj.addressManager); return x; @@ -61,8 +58,8 @@ Copayer.prototype.getPublicKey = function(path) { .toString(); }; -Copayer.prototype.getRWPubKey = function() { - return this.getPublicKey(RW_SIGNING_PATH); +Copayer.prototype.getRequestPubKey = function() { + return this.getPublicKey('m/1/1'); }; diff --git a/lib/server.js b/lib/server.js index ace2272..8b19e00 100644 --- a/lib/server.js +++ b/lib/server.js @@ -66,7 +66,7 @@ WalletService.getInstance = function() { * @param {Object} opts * @param {string} opts.copayerId - The copayer id making the request. * @param {string} opts.message - The contents of the request to be signed. - * @param {string} opts.signature - Signature of message to be verified using the copayer's rwPubKey + * @param {string} opts.signature - Signature of message to be verified using the copayer's requestPubKey */ WalletService.getInstanceWithAuth = function(opts, cb) { @@ -78,7 +78,7 @@ WalletService.getInstanceWithAuth = function(opts, cb) { if (err) return cb(err); if (!copayer) return cb(new ClientError('NOTAUTHORIZED', 'Copayer not found')); - var isValid = server._verifySignature(opts.message, opts.signature, copayer.rwPubKey); + var isValid = server._verifySignature(opts.message, opts.signature, copayer.requestPubKey); if (!isValid) return cb(new ClientError('NOTAUTHORIZED', 'Invalid signature')); @@ -312,7 +312,7 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); - var isValid = self._verifySignature(opts.message, opts.signature, copayer.rwPubKey); + var isValid = self._verifySignature(opts.message, opts.signature, copayer.requestPubKey); return cb(null, isValid); }); }; @@ -497,7 +497,7 @@ WalletService.prototype.createTx = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message); - if (!self._verifySignature(hash, opts.proposalSignature, copayer.rwPubKey)) + if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) return cb(new ClientError('Invalid proposal signature')); var toAddress; diff --git a/lib/storage.js b/lib/storage.js index 4e333f5..badd80b 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -81,8 +81,7 @@ Storage.prototype.storeWalletAndUpdateCopayersLookup = function(wallet, cb) { _.each(wallet.copayers, function(copayer) { var value = { walletId: wallet.id, - roPubKey: copayer.roPubKey, - rwPubKey: copayer.rwPubKey, + requestPubKey: copayer.requestPubKey, }; ops.push({ type: 'put', diff --git a/test/integration/client.js b/test/integration/client.js index 8df6f1b..21ba42b 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -949,7 +949,7 @@ describe('client API ', function() { }); }); - it.skip('should be able to sign from airgapped client and broadcast from proxy', function(done) { + it('should be able to sign from airgapped client and broadcast from proxy', function(done) { var airgapped = new AirGapped({ network: 'testnet' }); @@ -993,7 +993,12 @@ describe('client API ', function() { }, next); }, function(txps, rawTxps, next) { - var signatures = airgapped.signTxProposal(rawTxps[0]); + // TODO: these params should be grouped, signed, encrypted, etc + var pkr = proxy.credentials.publicKeyRing; + var m = proxy.credentials.m; + var n = proxy.credentials.n; + + var signatures = airgapped.signTxProposal(rawTxps[0], pkr, m, n); next(null, signatures); }, function(signatures, next) { From 265986e257fadd26b6ca3191b9880e5875da2cb7 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 10:27:26 -0300 Subject: [PATCH 04/13] pkr encryption --- lib/client/airgapped.js | 17 +++++++-- lib/client/api.js | 41 +++++++------------- lib/client/credentials.js | 1 + test/integration/client.js | 76 ++------------------------------------ 4 files changed, 33 insertions(+), 102 deletions(-) diff --git a/lib/client/airgapped.js b/lib/client/airgapped.js index 283ec6c..a096c2e 100644 --- a/lib/client/airgapped.js +++ b/lib/client/airgapped.js @@ -37,13 +37,24 @@ AirGapped.prototype.getSeed = function() { }; }; -AirGapped.prototype.signTxProposal = function(txp, pkr, m, n) { +AirGapped.prototype.signTxProposal = function(txp, encryptedPkr, m, n) { var self = this; - // TODO: complete credentials + var publicKeyRing; + try { + publicKeyRing = JSON.parse(WalletUtils.decryptMessage(encryptedPkr, self.credentials.personalEncryptingKey)); + } catch (ex) { + console.log(ex); + throw new Error('Could not decrypt public key ring'); + } + + if (!_.isArray(publicKeyRing) || publicKeyRing.length != n) { + throw new Error('Invalid public key ring'); + } + self.credentials.m = m; self.credentials.n = n; - self.credentials.addPublicKeyRing(pkr); + self.credentials.addPublicKeyRing(publicKeyRing); if (!Verifier.checkTxProposal(self.credentials, txp)) { throw new Error('Fake transaction proposal'); diff --git a/lib/client/api.js b/lib/client/api.js index f38c637..62ddcc8 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -461,30 +461,11 @@ API.prototype.getBalance = function(cb) { * */ -API.prototype.parseTxProposals = function(txData, cb) { - $.checkState(this.credentials && this.credentials.isComplete()); - - var self = this; - - var txps = txData.txps; - _processTxps(txps, self.credentials.sharedEncryptingKey); - - var fake = _.any(txps, function(txp) { - return (!Verifier.checkTxProposal(self.credentials, txp)); - }); - - if (fake) - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - - return cb(null, txps); -}; - - /** * * opts.doNotVerify - * opts.getRawTxps + * opts.forAirGapped * @return {undefined} */ @@ -497,11 +478,6 @@ API.prototype.getTxProposals = function(opts, cb) { self._doGetRequest(url, function(err, txps) { if (err) return cb(err); - var rawTxps; - if (opts.getRawTxps) { - rawTxps = JSON.parse(JSON.stringify(txps)); - } - _processTxps(txps, self.credentials.sharedEncryptingKey); var fake = _.any(txps, function(txp) { @@ -511,8 +487,19 @@ API.prototype.getTxProposals = function(opts, cb) { if (fake) return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - // TODO: return a single arg - return cb(null, txps, rawTxps); + var result; + if (opts.forAirGapped) { + result = { + txps: JSON.parse(JSON.stringify(txps)), + publicKeyRing: WalletUtils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey), + m: self.credentials.m, + n: self.credentials.n, + }; + } else { + result = txps; + } + + return cb(null, result); }); }; diff --git a/lib/client/credentials.js b/lib/client/credentials.js index 5af6a76..c4bd7fe 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -59,6 +59,7 @@ Credentials.prototype._expand = function() { this.xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); this.requestPrivKey = xPrivKey.derive('m/1/1').privateKey.toString(); } + this.personalEncryptingKey = WalletUtils.privateKeyToAESKey(this.requestPrivKey); this.copayerId = WalletUtils.xPubToCopayerId(this.xPubKey); }; diff --git a/test/integration/client.js b/test/integration/client.js index 21ba42b..1b0c8a1 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -865,69 +865,6 @@ describe('client API ', function() { }); describe('Air gapped related flows', function() { - it('should be able get Tx proposals from a file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(w) { - clients[0].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - var opts = { - amount: 10000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[1].sendTxProposal(opts, function(err, x) { - should.not.exist(err); - clients[1].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - clients[0].parseTxProposals({ - txps: rawTxps - }, function(err, txs2) { - should.not.exist(err); - txs[0].should.deep.equal(txs2[0]); - done(); - }); - - }); - }); - }); - }); - }); - it('should detect fakes from Tx proposals file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(w) { - clients[0].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - var opts = { - amount: 10000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[1].sendTxProposal(opts, function(err, x) { - should.not.exist(err); - clients[1].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - //Tamper - rawTxps[0].amount++; - - clients[0].parseTxProposals({ - txps: rawTxps - }, function(err, txs2) { - err.code.should.equal('SERVERCOMPROMISED'); - done(); - }); - - }); - }); - }); - }); - }); - it('should create wallet in proxy from airgapped', function(done) { var airgapped = new AirGapped({ network: 'testnet' @@ -989,16 +926,11 @@ describe('client API ', function() { }, function(txp, next) { proxy.getTxProposals({ - getRawTxps: true + forAirGapped: true }, next); }, - function(txps, rawTxps, next) { - // TODO: these params should be grouped, signed, encrypted, etc - var pkr = proxy.credentials.publicKeyRing; - var m = proxy.credentials.m; - var n = proxy.credentials.n; - - var signatures = airgapped.signTxProposal(rawTxps[0], pkr, m, n); + function(bundle, next) { + var signatures = airgapped.signTxProposal(bundle.txps[0], bundle.publicKeyRing, bundle.m, bundle.n); next(null, signatures); }, function(signatures, next) { @@ -1028,7 +960,7 @@ describe('client API ', function() { } ); }); - it.skip('should be able to detect tampered pkr when signing on airgapped client', function(done) {}); + it.skip('should be able to detect tampered PKR when signing on airgapped client', function(done) {}); it.skip('should be able to detect tampered proposal when signing on airgapped client', function(done) {}); it.skip('should be able to detect tampered change address when signing on airgapped client', function(done) {}); }); From d2085c9b9e446179b40b137a0cd6cc445b58700a Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 11:39:42 -0300 Subject: [PATCH 05/13] export/import --- lib/client/api.js | 134 ++++--------------------------------- lib/client/credentials.js | 46 +++++++++++++ test/integration/client.js | 26 +++++++ 3 files changed, 85 insertions(+), 121 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 62ddcc8..33346c3 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -192,11 +192,13 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb var self = this; network = network || 'livenet'; - if (!_.contains(['testnet', 'livenet'], network)) - return cb('Invalid network'); + if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); if (!self.credentials) { + log.info('Generating new keys'); self.credentials = Credentials.create(network); + } else { + log.info('Using existing keys'); } $.checkState(network == self.credentials.network); @@ -226,39 +228,6 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb }); }; - -// API.prototype.reCreateWallet = function(walletName, cb) { -// var self = this; -// this._loadAndCheck(function(err, wcd) { -// if (err) return cb(err); - -// var walletPrivKey = new Bitcore.PrivateKey(); -// var args = { -// name: walletName, -// m: wcd.m, -// n: wcd.n, -// pubKey: walletPrivKey.toPublicKey().toString(), -// network: wcd.network, -// }; -// var url = '/v1/wallets/'; -// self._doPostRequest(url, args, function(err, body) { -// if (err) return cb(err); - -// var walletId = body.walletId; - -// var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network); -// var i = 0; -// async.each(wcd.publicKeyRing, function(xpub, next) { -// var copayerName = 'recovered Copayer #' + i; -// self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next); -// }, function(err) { -// return cb(err); -// }); -// }); -// }); -// }; - - API.prototype.joinWallet = function(secret, copayerName, cb) { var self = this; @@ -371,96 +340,19 @@ API.prototype.getBalance = function(cb) { }; /** - * Export does not try to complete the wallet from the server. Exports the - * wallet as it is now. - * - * @param opts.access =['full', 'readonly', 'readwrite'] + * Exports the wallet as it is now. */ -// API.prototype.export = function(opts, cb) { -// $.checkState(this.credentials); -// $.shouldBeFunction(cb); - -// var self = this; - -// opts = opts || {}; -// var access = opts.access || 'full'; - -// this._load(function(err, wcd) { -// if (err) return cb(err); -// var v = []; - -// var myXPubKey = wcd.xPrivKey ? (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString() : ''; - -// _.each(WALLET_CRITICAL_DATA, function(k) { -// var d; - -// if (access != 'full' && k === 'xPrivKey') { -// v.push(null); -// return; -// } - -// // Skips own pub key IF priv key is exported -// if (access == 'full' && k === 'publicKeyRing') { -// d = _.without(wcd[k], myXPubKey); -// } else { -// d = wcd[k]; -// } -// v.push(d); -// }); - -// if (access != 'full') { -// v.push(wcd.copayerId); -// v.push(wcd.roPrivKey); -// if (access == 'readwrite') { -// v.push(wcd.requestPrivKey); -// } -// } - -// return cb(null, JSON.stringify(v)); -// }); -// } - - -// API.prototype.import = function(str, cb) { -// var self = this; - -// this.storage.load(function(err, wcd) { -// if (wcd) -// return cb('Storage already contains a wallet'); - -// wcd = {}; - -// var inData = JSON.parse(str); -// var i = 0; - -// _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { -// wcd[k] = inData[i++]; -// }); - -// if (wcd.xPrivKey) { -// var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey); -// var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); -// wcd.publicKeyRing.unshift(xPubKey); -// wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey); -// wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); -// wcd.requestPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); -// } - -// if (!wcd.publicKeyRing) -// return cb('Invalid source wallet'); +API.prototype.export = function() { + $.checkState(this.credentials); -// wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; + return this.credentials.exportCompressed(); +} -// self.save(wcd, function(err) { -// return cb(err, WalletUtils.accessFromData(wcd)); -// }); -// }); -// }; - -/** - * - */ +API.prototype.import = function(str) { + this.credentials = new Credentials(); + this.credentials.importCompressed(str); +}; /** * diff --git a/lib/client/credentials.js b/lib/client/credentials.js index c4bd7fe..2798b8b 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -21,6 +21,16 @@ var FIELDS = [ 'copayerName', ]; +var EXPORTABLE_FIELDS = [ + 'xPrivKey', + 'requestPrivKey', + 'xPubKey', + 'm', + 'n', + 'publicKeyRing', + 'sharedEncryptingKey', +]; + function Credentials() { this.version = '1.0.0'; }; @@ -104,6 +114,42 @@ Credentials.prototype.isComplete = function() { return true; }; +Credentials.prototype.exportCompressed = function() { + var self = this; + + var values = _.map(EXPORTABLE_FIELDS, function(field) { + if ((field == 'xPubKey' || field == 'requestPrivKey') && self.canSign()) return; + if (field == 'publicKeyRing') { + return _.without(self.publicKeyRing, self.xPubKey); + } + return self[field]; + }); + values.unshift(self.version); + + return JSON.stringify(values); +}; + +Credentials.prototype.importCompressed = function(compressed) { + var self = this; + + var list; + try { + list = JSON.parse(compressed); + } catch (ex) { + throw new Error('Invalid string'); + } + + // Remove version + var version = list[0]; + list = _.rest(list); + _.each(EXPORTABLE_FIELDS, function(field, i) { + self[field] = list[i]; + }); + self._expand(); + + self.network = self.xPubKey.substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; + self.publicKeyRing.push(self.xPubKey); +}; module.exports = Credentials; diff --git a/test/integration/client.js b/test/integration/client.js index 1b0c8a1..63bc0fc 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -864,6 +864,32 @@ describe('client API ', function() { it.skip('should get paginated transaction history', function(done) {}); }); + describe('Export & Import', function() { + it('should export & import', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { + clients[0].createAddress(function(err, address) { + should.not.exist(err); + should.exist(address.address); + + var exported = clients[0].export(); + + var importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported); + + importedClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list.length.should.equal(1); + list[0].address.should.equal(address.address); + done(); + }); + }); + }) + }); + }); + describe('Air gapped related flows', function() { it('should create wallet in proxy from airgapped', function(done) { var airgapped = new AirGapped({ From d6ac0e4105dde69f67f3393445931af296fb3f2c Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 12:45:10 -0300 Subject: [PATCH 06/13] make importCompressed static --- lib/client/api.js | 3 +-- lib/client/credentials.js | 17 +++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 33346c3..452c9a1 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -350,8 +350,7 @@ API.prototype.export = function() { API.prototype.import = function(str) { - this.credentials = new Credentials(); - this.credentials.importCompressed(str); + this.credentials = Credentials.importCompressed(str); }; /** diff --git a/lib/client/credentials.js b/lib/client/credentials.js index 2798b8b..e16d9fe 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -129,27 +129,28 @@ Credentials.prototype.exportCompressed = function() { return JSON.stringify(values); }; -Credentials.prototype.importCompressed = function(compressed) { - var self = this; - +Credentials.importCompressed = function(compressed) { var list; try { list = JSON.parse(compressed); } catch (ex) { - throw new Error('Invalid string'); + throw new Error('Invalid compressed format'); } + var x = new Credentials(); + // Remove version var version = list[0]; list = _.rest(list); _.each(EXPORTABLE_FIELDS, function(field, i) { - self[field] = list[i]; + x[field] = list[i]; }); - self._expand(); + x._expand(); - self.network = self.xPubKey.substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; - self.publicKeyRing.push(self.xPubKey); + x.network = x.xPubKey.substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; + x.publicKeyRing.push(x.xPubKey); + return x; }; module.exports = Credentials; From 18884f3c0f00636bace8a21a29247775270cd36e Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 13:00:05 -0300 Subject: [PATCH 07/13] extract network from xpub --- lib/client/airgapped.js | 7 ++----- lib/client/api.js | 39 +++++++++++++++++++-------------------- lib/client/credentials.js | 15 ++++++++++----- lib/walletutils.js | 4 ++++ 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/client/airgapped.js b/lib/client/airgapped.js index a096c2e..8bde1e0 100644 --- a/lib/client/airgapped.js +++ b/lib/client/airgapped.js @@ -28,12 +28,9 @@ function AirGapped(opts) { util.inherits(AirGapped, events.EventEmitter); AirGapped.prototype.getSeed = function() { - var cred = this.credentials; - return { - network: cred.network, - xPubKey: cred.xPubKey, - requestPrivKey: cred.requestPrivKey, + xPubKey: this.credentials.xPubKey, + requestPrivKey: this.credentials.requestPrivKey, }; }; diff --git a/lib/client/api.js b/lib/client/api.js index 452c9a1..ac69f44 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -87,8 +87,12 @@ function API(opts) { util.inherits(API, events.EventEmitter); +API.prototype.seedFromExtendedPrivateKey = function(xPrivKey) { + this.credentials = Credentials.seedFromExtendedPrivateKey(xPrivKey); +}; + API.prototype.seedFromAirGapped = function(seed) { - this.credentials = Credentials.fromAirGapped(seed.network, seed.xPubKey, seed.requestPrivKey); + this.credentials = Credentials.fromExtendedPublicKey(seed.xPubKey, seed.requestPrivKey); }; API.prototype._doRequest = function(method, url, args, cb) { @@ -140,6 +144,9 @@ API.prototype._doGetRequest = function(url, cb) { return this._doRequest('get', url, {}, cb); }; +API.prototype._doDeleteRequest = function(url, cb) { + return this._doRequest('delete', url, {}, cb); +}; API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) { var args = { @@ -170,8 +177,7 @@ API.prototype.openWallet = function(cb) { if (self.credentials.isComplete()) return cb(null, false); - var url = '/v1/wallets/'; - self._doGetRequest(url, function(err, ret) { + self._doGetRequest('/v1/wallets/', function(err, ret) { if (err) return cb(err); var wallet = ret.wallet; @@ -211,8 +217,8 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb pubKey: walletPrivKey.toPublicKey().toString(), network: network, }; - var url = '/v1/wallets/'; - self._doPostRequest(url, args, function(err, body) { + + self._doPostRequest('/v1/wallets/', args, function(err, body) { if (err) return cb(err); var walletId = body.walletId; @@ -253,8 +259,7 @@ API.prototype.getStatus = function(cb) { $.checkState(this.credentials && this.credentials.isComplete()); var self = this; - var url = '/v1/wallets/'; - self._doGetRequest(url, function(err, result) { + self._doGetRequest('/v1/wallets/', function(err, result) { _processTxps(result.pendingTxps, self.credentials.sharedEncryptingKey); return cb(err, result, self.credentials.copayerId); }); @@ -284,8 +289,7 @@ API.prototype.sendTxProposal = function(opts, cb) { args.proposalSignature = WalletUtils.signMessage(hash, self.credentials.requestPrivKey); log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); - var url = '/v1/txproposals/'; - self._doPostRequest(url, args, function(err, txp) { + self._doPostRequest('/v1/txproposals/', args, function(err, txp) { if (err) return cb(err); return cb(null, txp); }); @@ -296,8 +300,7 @@ API.prototype.createAddress = function(cb) { var self = this; - var url = '/v1/addresses/'; - self._doPostRequest(url, {}, function(err, address) { + self._doPostRequest('/v1/addresses/', {}, function(err, address) { if (err) return cb(err); if (!Verifier.checkAddress(self.credentials, address)) { return cb(new ServerCompromisedError('Server sent fake address')); @@ -316,8 +319,7 @@ API.prototype.getMainAddresses = function(opts, cb) { var self = this; - var url = '/v1/addresses/'; - self._doGetRequest(url, function(err, addresses) { + self._doGetRequest('/v1/addresses/', function(err, addresses) { if (err) return cb(err); if (!opts.doNotVerify) { @@ -335,8 +337,7 @@ API.prototype.getBalance = function(cb) { $.checkState(this.credentials && this.credentials.isComplete()); var self = this; - var url = '/v1/balance/'; - self._doGetRequest(url, cb); + self._doGetRequest('/v1/balance/', cb); }; /** @@ -365,8 +366,7 @@ API.prototype.getTxProposals = function(opts, cb) { var self = this; - var url = '/v1/txproposals/'; - self._doGetRequest(url, function(err, txps) { + self._doGetRequest('/v1/txproposals/', function(err, txps) { if (err) return cb(err); _processTxps(txps, self.credentials.sharedEncryptingKey); @@ -472,7 +472,7 @@ API.prototype.removeTxProposal = function(txp, cb) { var self = this; var url = '/v1/txproposals/' + txp.id; - self._doRequest('delete', url, {}, function(err) { + self._doDeleteRequest(url, function(err) { if (err) return cb(err); return cb(); }); @@ -483,8 +483,7 @@ API.prototype.getTxHistory = function(opts, cb) { var self = this; - var url = '/v1/txhistory/'; - self._doGetRequest(url, function(err, txs) { + self._doGetRequest('/v1/txhistory/', function(err, txs) { if (err) return cb(err); _processTxps(txs, self.credentials.sharedEncryptingKey); diff --git a/lib/client/credentials.js b/lib/client/credentials.js index e16d9fe..ae3c836 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -44,17 +44,15 @@ Credentials.create = function(network) { return x; }; -Credentials.fromExtendedPrivateKey = function(network, xPrivKey) { +Credentials.fromExtendedPrivateKey = function(xPrivKey) { var x = new Credentials(); - x.network = network; x.xPrivKey = xPrivKey; x._expand(); return x; }; -Credentials.fromAirGapped = function(network, xPubKey, requestPrivKey) { +Credentials.fromExtendedPublicKey = function(xPubKey, requestPrivKey) { var x = new Credentials(); - x.network = network; x.xPubKey = xPubKey; x.requestPrivKey = requestPrivKey; x._expand(); @@ -69,6 +67,13 @@ Credentials.prototype._expand = function() { this.xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); this.requestPrivKey = xPrivKey.derive('m/1/1').privateKey.toString(); } + var network = WalletUtils.getNetworkFromXPubKey(this.xPubKey); + if (this.network) { + $.checkState(this.network == network); + } else { + this.network = network; + } + this.personalEncryptingKey = WalletUtils.privateKeyToAESKey(this.requestPrivKey); this.copayerId = WalletUtils.xPubToCopayerId(this.xPubKey); }; @@ -148,7 +153,7 @@ Credentials.importCompressed = function(compressed) { }); x._expand(); - x.network = x.xPubKey.substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; + x.network = WalletUtils.getNetworkFromXPubKey(x.xPubKey); x.publicKeyRing.push(x.xPubKey); return x; }; diff --git a/lib/walletutils.js b/lib/walletutils.js index acf5397..2dc853e 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -205,4 +205,8 @@ WalletUtils.signTxp = function(txp, xPrivKey) { return signatures; }; +WalletUtils.getNetworkFromXPubKey = function(xPubKey) { + return xPubKey.substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; +}; + module.exports = WalletUtils; From a0019d966c8beed5cd7790819ec90e2573129572 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 14:05:06 -0300 Subject: [PATCH 08/13] import & export with compress/encrypt --- lib/client/api.js | 84 +++++++++++++++++++++++++++++------ lib/walletutils.js | 34 -------------- test/integration/client.js | 90 +++++++++++++++++++++++++++++++------- 3 files changed, 144 insertions(+), 64 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index ac69f44..8f07729 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -9,6 +9,7 @@ var request = require('request') var events = require('events'); log.debug = log.verbose; var Bitcore = require('bitcore') +var sjcl = require('sjcl'); var Credentials = require('./credentials'); var WalletUtils = require('../walletutils'); @@ -17,6 +18,9 @@ var ServerCompromisedError = require('./servercompromisederror'); var ClientError = require('../clienterror'); var BASE_URL = 'http://localhost:3001/copay/api'; +var WALLET_ENCRYPTION_OPTS = { + iter: 5000 +}; function _encryptMessage(message, encryptingKey) { if (!message) return null; @@ -95,6 +99,73 @@ API.prototype.seedFromAirGapped = function(seed) { this.credentials = Credentials.fromExtendedPublicKey(seed.xPubKey, seed.requestPrivKey); }; +/** + * export + * + * @param opts + * @param opts.compressed + * @param opts.password + */ +API.prototype.export = function(opts) { + $.checkState(this.credentials); + + opts = opts || {}; + + var output; + if (opts.compressed) { + output = this.credentials.exportCompressed(); + } else { + output = JSON.stringify(this.credentials.toObj()); + } + + if (opts.password) { + output = sjcl.encrypt(opts.password, output, WALLET_ENCRYPTION_OPTS); + } + + return output; +} + + +/** + * export + * + * @param opts + * @param opts.compressed + * @param opts.password + */ +API.prototype.import = function(str, opts) { + opts = opts || {}; + + var input = str; + if (opts.password) { + try { + input = sjcl.decrypt(opts.password, input); + } catch (ex) { + throw new Error('Incorrect password'); + } + } + + try { + if (opts.compressed) { + this.credentials = Credentials.importCompressed(input); + // TODO: complete missing fields that live on the server only such as: walletId, walletName, copayerName + } else { + this.credentials = Credentials.fromObj(JSON.parse(input)); + } + } catch (ex) { + throw new Error('Error importing from source'); + } +}; + +API.prototype.toString = function(password) { + $.checkState(this.credentials); + return this.credentials.toObject(); +}; + +API.prototype.fromString = function(str) { + this.credentials = Credentials.fromObject(str); +}; + API.prototype._doRequest = function(method, url, args, cb) { $.checkState(this.credentials); @@ -340,19 +411,6 @@ API.prototype.getBalance = function(cb) { self._doGetRequest('/v1/balance/', cb); }; -/** - * Exports the wallet as it is now. - */ -API.prototype.export = function() { - $.checkState(this.credentials); - - return this.credentials.exportCompressed(); -} - - -API.prototype.import = function(str) { - this.credentials = Credentials.importCompressed(str); -}; /** * diff --git a/lib/walletutils.js b/lib/walletutils.js index 2dc853e..4958b9d 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -133,40 +133,6 @@ WalletUtils.privateKeyToAESKey = function(privKey) { return Bitcore.crypto.Hash.sha256(pk.toBuffer()).slice(0, 16).toString('base64'); }; -WalletUtils.decryptWallet = function(data, password) { - $.checkArgument(data.enc); - var extraFields = JSON.parse(sjcl.decrypt(password, data.enc)); - delete data.enc; - return _.extend(data, extraFields); -}; - - -WalletUtils.sjclOpts = { - iter: 5000, -}; - -WalletUtils.encryptWallet = function(data, accessWithoutEncrytion, password) { - - // Fields to encrypt, given the NOPASSWD access level - var fieldsEncryptByLevel = { - none: _.keys(data), - readonly: ['xPrivKey', 'requestPrivKey', 'publicKeyRing'], - readwrite: ['xPrivKey', ], - full: [], - }; - - var fieldsEncrypt = fieldsEncryptByLevel[accessWithoutEncrytion]; - $.checkState(!_.isUndefined(fieldsEncrypt)); - - var toEncrypt = _.pick(data, fieldsEncrypt); - var enc = sjcl.encrypt(password, JSON.stringify(toEncrypt), WalletUtils.sjclOpts); - - var ret = _.omit(data, fieldsEncrypt); - ret.enc = enc; - return ret; -}; - - WalletUtils.signTxp = function(txp, xPrivKey) { var self = this; diff --git a/test/integration/client.js b/test/integration/client.js index 63bc0fc..e923a27 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -865,29 +865,85 @@ describe('client API ', function() { }); describe('Export & Import', function() { - it('should export & import', function(done) { + var address, importedClient; + beforeEach(function(done) { + importedClient = null; helpers.createAndJoinWallet(clients, 1, 1, function() { - clients[0].createAddress(function(err, address) { + clients[0].createAddress(function(err, addr) { should.not.exist(err); - should.exist(address.address); + should.exist(addr.address); + address = addr.address; + done(); + }); + }); + }); + afterEach(function(done) { + importedClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list.length.should.equal(1); + list[0].address.should.equal(address); + done(); + }); + }); - var exported = clients[0].export(); + it('should export & import', function() { + var exported = clients[0].export(); - var importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported); + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported); + }); + it.skip('should export & import compressed', function() { + var walletId = clients[0].credentials.walletId; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; - importedClient.getMainAddresses({}, function(err, list) { - should.not.exist(err); - should.exist(list); - list.length.should.equal(1); - list[0].address.should.equal(address.address); - done(); - }); - }); - }) + var exported = clients[0].export({ + compressed: true + }); + + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported, { + compressed: true + }); + importedClient.credentials.walletId.should.equal(walletId); + importedClient.credentials.walletName.should.equal(walletName); + importedClient.credentials.copayerName.should.equal(copayerName); + }); + it('should export & import encrypted', function() { + var exported = clients[0].export({ + password: '123' + }); + + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported, { + password: '123' + }); + }); + it('should export & import compressed & encrypted', function() { + var exported = clients[0].export({ + compressed: true, + password: '123' + }); + + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported, { + compressed: true, + password: '123' + }); }); + it.skip('should fail to export compressed & import uncompressed', function() {}); + it.skip('should fail to export uncompressed & import compressed', function() {}); + it.skip('should fail to export unencrypted & import with password', function() {}); + it.skip('should fail to export encrypted & import with incorrect password', function() {}); }); describe('Air gapped related flows', function() { From 5b5aca9970229d8e71836d645c1575600de94663 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 15:11:29 -0300 Subject: [PATCH 09/13] export privKeys in WIF --- lib/client/credentials.js | 5 ++++- test/integration/client.js | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/client/credentials.js b/lib/client/credentials.js index ae3c836..4a8806d 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -123,7 +123,10 @@ Credentials.prototype.exportCompressed = function() { var self = this; var values = _.map(EXPORTABLE_FIELDS, function(field) { - if ((field == 'xPubKey' || field == 'requestPrivKey') && self.canSign()) return; + if ((field == 'xPubKey' || field == 'requestPrivKey') && self.canSign()) return ''; + if (field == 'requestPrivKey') { + return Bitcore.PrivateKey.fromString(self.requestPrivKey).toWIF(); + } if (field == 'publicKeyRing') { return _.without(self.publicKeyRing, self.xPubKey); } diff --git a/test/integration/client.js b/test/integration/client.js index e923a27..1480c06 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -915,9 +915,13 @@ describe('client API ', function() { importedClient.credentials.copayerName.should.equal(copayerName); }); it('should export & import encrypted', function() { + var xPrivKey = clients[0].credentials.xPrivKey; + should.exist(xPrivKey); + var exported = clients[0].export({ password: '123' }); + exported.should.not.contain(xPrivKey); importedClient = new Client({ request: helpers.getRequest(app), @@ -925,6 +929,8 @@ describe('client API ', function() { importedClient.import(exported, { password: '123' }); + should.exist(importedClient.credentials.xPrivKey); + importedClient.credentials.xPrivKey.should.equal(xPrivKey); }); it('should export & import compressed & encrypted', function() { var exported = clients[0].export({ From 6dcd38746147454a171fa39c65ea7e158b6e31ef Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 15:43:37 -0300 Subject: [PATCH 10/13] bit-create & bit-join --- bit-wallet/bit-create | 16 +++++--- bit-wallet/bit-join | 10 +++-- bit-wallet/cli-utils.js | 86 ++++++++++++++++++++++----------------- bit-wallet/filestorage.js | 30 ++++++++++++++ lib/client/api.js | 6 ++- 5 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 bit-wallet/filestorage.js diff --git a/bit-wallet/bit-create b/bit-wallet/bit-create index ebbe5ee..75f2b7b 100755 --- a/bit-wallet/bit-create +++ b/bit-wallet/bit-create @@ -27,10 +27,14 @@ try { utils.die(ex); } -var client = utils.getClient(program); -client.createWallet(walletName, copayerName, mn[0], mn[1], network, function(err, secret) { - utils.die(err); - console.log(' * ' + _.capitalize(network) + ' Wallet Created.'); - if (secret) - console.log(' - Secret to share:\n\t' + secret); +utils.getClient(program, function (client) { + client.createWallet(walletName, copayerName, mn[0], mn[1], network, function(err, secret) { + utils.die(err); + console.log(' * ' + _.capitalize(network) + ' Wallet Created.'); + utils.saveClient(program, client, function () { + if (secret) { + console.log(' - Secret to share:\n\t' + secret); + } + }); + }); }); diff --git a/bit-wallet/bit-join b/bit-wallet/bit-join index fb169b6..6834b08 100755 --- a/bit-wallet/bit-join +++ b/bit-wallet/bit-join @@ -17,8 +17,10 @@ if (!args[0]) var secret = args[0]; var copayerName = args[1] || process.env.USER; -var client = utils.getClient(program); -client.joinWallet(secret, copayerName, function(err, xx) { - utils.die(err); - console.log(' * Wallet Joined.', xx || ''); +utils.getClient(program, function (client) { + client.joinWallet(secret, copayerName, function(err, wallet) { + utils.die(err); + console.log(' * Wallet Joined.', wallet.name); + utils.saveClient(program, client, function () {}); + }); }); diff --git a/bit-wallet/cli-utils.js b/bit-wallet/cli-utils.js index f6c65e8..44e7b02 100644 --- a/bit-wallet/cli-utils.js +++ b/bit-wallet/cli-utils.js @@ -1,5 +1,6 @@ var _ = require('lodash'); var Client = require('../lib/client'); +var FileStorage = require('./filestorage'); var read = require('read') var Utils = function() {}; @@ -39,55 +40,66 @@ Utils.confirmationId = function(copayer) { return parseInt(copayer.xPubKeySignature.substr(-4), 16).toString().substr(-4); } -Utils.getClient = function(args) { - var storage = new Client.FileStorage({ +Utils.getClient = function(args, cb) { + var storage = new FileStorage({ filename: args.file || process.env['BIT_FILE'], }); - var c = new Client({ - storage: storage, + var client = new Client({ baseUrl: args.host || process.env['BIT_HOST'], verbose: args.verbose, }); - - - if (args.nopasswd) - c.setNopasswdAccess(args.nopasswd); - - var setPassword; - c.on('needPassword', function(cb) { - if (args.password) { - return cb(args.password); - } else { - if (setPassword) - return cb(setPassword); - - read({ - prompt: 'Password for ' + args.file + ' : ', - silent: true - }, function(er, password) { - setPassword = password; - return cb(password); - }) + storage.load(function(err, walletData) { + if (err && err.code != 'ENOENT') die(err); + if (walletData) { + client.import(walletData); } + return cb(client); }); +}; - c.on('needNewPassword', function(cb) { - if (args.password) { - return cb(args.password); - } else { - read({ - prompt: 'New Password: ', - silent: true - }, function(er, password) { - return cb(password); - }) - } +Utils.saveClient = function(args, client, cb) { + var storage = new FileStorage({ + filename: args.file || process.env['BIT_FILE'], + }); + var str = client.export(); + storage.save(str, function(err) { + die(err); + return cb(); }); +}; +// var setPassword; +// c.on('needPassword', function(cb) { +// if (args.password) { +// return cb(args.password); +// } else { +// if (setPassword) +// return cb(setPassword); + +// read({ +// prompt: 'Password for ' + args.file + ' : ', +// silent: true +// }, function(er, password) { +// setPassword = password; +// return cb(password); +// }) +// } +// }); + +// c.on('needNewPassword', function(cb) { +// if (args.password) { +// return cb(args.password); +// } else { +// read({ +// prompt: 'New Password: ', +// silent: true +// }, function(er, password) { +// return cb(password); +// }) +// } +// }); - return c; -} Utils.findOneTxProposal = function(txps, id) { var matches = _.filter(txps, function(tx) { diff --git a/bit-wallet/filestorage.js b/bit-wallet/filestorage.js new file mode 100644 index 0000000..021a774 --- /dev/null +++ b/bit-wallet/filestorage.js @@ -0,0 +1,30 @@ +var fs = require('fs') + +function FileStorage(opts) { + if (!opts.filename) { + throw new Error('Please set wallet filename'); + } + this.filename = opts.filename; + this.fs = opts.fs || fs; +}; + +FileStorage.prototype.getName = function() { + return this.filename; +}; + +FileStorage.prototype.save = function(data, cb) { + this.fs.writeFile(this.filename, JSON.stringify(data), cb); +}; + +FileStorage.prototype.load = function(cb) { + this.fs.readFile(this.filename, 'utf8', function(err, data) { + if (err) return cb(err); + try { + data = JSON.parse(data); + } catch (e) {} + return cb(null, data); + }); +}; + + +module.exports = FileStorage; diff --git a/lib/client/api.js b/lib/client/api.js index 8f07729..97635a3 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -145,16 +145,18 @@ API.prototype.import = function(str, opts) { } } + var credentials; try { if (opts.compressed) { - this.credentials = Credentials.importCompressed(input); + credentials = Credentials.importCompressed(input); // TODO: complete missing fields that live on the server only such as: walletId, walletName, copayerName } else { - this.credentials = Credentials.fromObj(JSON.parse(input)); + credentials = Credentials.fromObj(JSON.parse(input)); } } catch (ex) { throw new Error('Error importing from source'); } + this.credentials = credentials; }; API.prototype.toString = function(password) { From c0b7970ff6b52d5a4245242f4811e2da13ad64e4 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 15:58:17 -0300 Subject: [PATCH 11/13] fix bit client: create, join, status --- bit-wallet/bit-create | 1 - bit-wallet/bit-join | 1 - bit-wallet/bit-status | 24 ++++++++++++------------ bit-wallet/cli-utils.js | 18 ++++++++++++++---- lib/client/api.js | 2 +- test/integration/client.js | 1 + 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/bit-wallet/bit-create b/bit-wallet/bit-create index 75f2b7b..6fb2bdd 100755 --- a/bit-wallet/bit-create +++ b/bit-wallet/bit-create @@ -8,7 +8,6 @@ program = utils.configureCommander(program); program .option('-t, --testnet', 'Create a Testnet Wallet') - .option('-n, --nopasswd [level]', 'Set access for no password usage: none(default), readonly, readwrite, full', 'none') .usage('[options] [copayerName]') .parse(process.argv); diff --git a/bit-wallet/bit-join b/bit-wallet/bit-join index 6834b08..1429b38 100755 --- a/bit-wallet/bit-join +++ b/bit-wallet/bit-join @@ -7,7 +7,6 @@ program = utils.configureCommander(program); program .usage('[options] [copayerName]') - .option('-n, --nopasswd [level]', 'Set access for no password usage: none(default), readonly, readwrite, full', 'none') .parse(process.argv); var args = program.args; diff --git a/bit-wallet/bit-status b/bit-wallet/bit-status index 43faaa5..37bd1e2 100755 --- a/bit-wallet/bit-status +++ b/bit-wallet/bit-status @@ -9,20 +9,20 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); +utils.getClient(program, function (client) { + client.getStatus(function(err, res) { + utils.die(err); -client.getStatus(function(err, res) { - utils.die(err); + var x = res.wallet; + console.log('* Wallet %s [%s]: %d-%d %s ', x.name, x.network, x.m, x.n, x.status); - var x = res.wallet; - console.log('* Wallet %s [%s]: %d-%d %s ', x.name, x.network, x.m, x.n, x.status); + if (x.status != 'complete') + console.log(' Missing copayers:', x.n - x.copayers.length); + console.log('* Copayers:', _.pluck(x.copayers,'name').join(', ')); - if (x.status != 'complete') - console.log(' Missing copayers:', x.n - x.copayers.length); - console.log('* Copayers:', _.pluck(x.copayers,'name').join(', ')); + var x = res.balance; + console.log('* Balance %s (Locked: %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount)); - var x = res.balance; - console.log('* Balance %s (Locked: %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount)); - - utils.renderTxProposals(res.pendingTxps); + utils.renderTxProposals(res.pendingTxps); + }); }); diff --git a/bit-wallet/cli-utils.js b/bit-wallet/cli-utils.js index 44e7b02..4613a19 100644 --- a/bit-wallet/cli-utils.js +++ b/bit-wallet/cli-utils.js @@ -2,6 +2,7 @@ var _ = require('lodash'); var Client = require('../lib/client'); var FileStorage = require('./filestorage'); var read = require('read') +var log = require('npmlog'); var Utils = function() {}; @@ -50,10 +51,19 @@ Utils.getClient = function(args, cb) { }); storage.load(function(err, walletData) { if (err && err.code != 'ENOENT') die(err); - if (walletData) { - client.import(walletData); - } - return cb(client); + if (!walletData) return cb(client); + + client.import(walletData); + client.openWallet(function(err, justCompleted) { + if (client.isComplete() && justCompleted) { + Utils.saveClient(args, client, function() { + log.info('Your wallet has just been completed. Please backup your wallet file or use the export command.'); + return cb(client); + }); + } else { + return cb(client); + } + }); }); }; diff --git a/lib/client/api.js b/lib/client/api.js index 97635a3..9b3b03f 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -329,7 +329,7 @@ API.prototype.joinWallet = function(secret, copayerName, cb) { }; API.prototype.getStatus = function(cb) { - $.checkState(this.credentials && this.credentials.isComplete()); + $.checkState(this.credentials); var self = this; self._doGetRequest('/v1/wallets/', function(err, result) { diff --git a/test/integration/client.js b/test/integration/client.js index 1480c06..a2ad67e 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -324,6 +324,7 @@ describe('client API ', function() { }); }); }); + it.skip('should return wallet status even if wallet is not yet complete', function(done) {}); }); describe('Address Creation', function() { From f486ecacd31e461c969f22e5e2f34f7464678d29 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 17:53:34 -0300 Subject: [PATCH 12/13] test regaining access to wallet with only xPrivKey --- lib/client/api.js | 22 ++-- lib/client/credentials.js | 4 + lib/client/verifier.js | 1 + test/integration/client.js | 207 +++++++++++++++++++++---------------- 4 files changed, 138 insertions(+), 96 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 9b3b03f..fc73851 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -92,7 +92,7 @@ function API(opts) { util.inherits(API, events.EventEmitter); API.prototype.seedFromExtendedPrivateKey = function(xPrivKey) { - this.credentials = Credentials.seedFromExtendedPrivateKey(xPrivKey); + this.credentials = Credentials.fromExtendedPrivateKey(xPrivKey); }; API.prototype.seedFromAirGapped = function(seed) { @@ -254,15 +254,25 @@ API.prototype.openWallet = function(cb) { if (err) return cb(err); var wallet = ret.wallet; - if (wallet.status != 'complete') - return cb('Wallet Incomplete'); + if (wallet.status != 'complete') return cb('Wallet Incomplete'); - if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { - return cb(new ServerCompromisedError( - 'Copayers in the wallet could not be verified to have known the wallet secret')); + if (!!self.credentials.walletPrivKey) { + if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { + return cb(new ServerCompromisedError( + 'Copayers in the wallet could not be verified to have known the wallet secret')); + } + } else { + log.warn('Could not perform verification of other copayers in the wallet'); } self.credentials.addPublicKeyRing(_.pluck(wallet.copayers, 'xPubKey')); + if (!self.credentials.hasWalletInfo()) { + var me = _.find(wallet.copayers, { + id: self.credentials.copayerId + }); + self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, null, me.name); + } + return cb(null, true); }); }; diff --git a/lib/client/credentials.js b/lib/client/credentials.js index 4a8806d..29f9373 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -105,6 +105,10 @@ Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, walle } }; +Credentials.prototype.hasWalletInfo = function() { + return !!this.walletId; +}; + Credentials.prototype.addPublicKeyRing = function(publicKeyRing) { this.publicKeyRing = _.clone(publicKeyRing); }; diff --git a/lib/client/verifier.js b/lib/client/verifier.js index 84f1518..f45f101 100644 --- a/lib/client/verifier.js +++ b/lib/client/verifier.js @@ -18,6 +18,7 @@ Verifier.checkAddress = function(credentials, address) { }; Verifier.checkCopayers = function(credentials, copayers) { + $.checkState(credentials.walletPrivKey); var walletPubKey = Bitcore.PrivateKey.fromString(credentials.walletPrivKey).toPublicKey().toString(); if (copayers.length != credentials.n) { diff --git a/test/integration/client.js b/test/integration/client.js index a2ad67e..0f4adac 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -1,6 +1,7 @@ 'use strict'; var _ = require('lodash'); +var $ = require('preconditions').singleton(); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); @@ -20,6 +21,7 @@ var TestData = require('../testdata'); var helpers = {}; helpers.getRequest = function(app) { + $.checkArgument(app); return function(args, cb) { var req = request(app); var r = req[args.method](args.relUrl); @@ -38,6 +40,13 @@ helpers.getRequest = function(app) { }; }; +helpers.newClient = function(app) { + $.checkArgument(app); + return new Client({ + request: helpers.getRequest(app), + }); +}; + helpers.createAndJoinWallet = function(clients, m, n, cb) { clients[0].createWallet('wallet name', 'creator', m, n, 'testnet', function(err, secret) { @@ -148,9 +157,7 @@ describe('client API ', function() { }); // Generates 5 clients clients = _.map(_.range(5), function(i) { - return new Client({ - request: helpers.getRequest(app), - }); + return helpers.newClient(app); }); blockExplorerMock.reset(); }); @@ -179,10 +186,8 @@ describe('client API ', function() { }); var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); - var client = new Client({ - storage: s2, - }); - client.request = helpers.getRequest(app); + var client = helpers.newClient(app); + client.storage = s2; client.createWallet('1', '2', 1, 1, 'testnet', function(err) { err.code.should.equal('ERROR'); @@ -206,10 +211,8 @@ describe('client API ', function() { }); var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); - var client = new Client({ - storage: s2, - }); - client.request = helpers.getRequest(app); + var client = helpers.newClient(app); + client.storage = s2; client.createWallet('1', '2', 1, 1, 'testnet', function(err) { err.code.should.equal('ERROR'); @@ -865,92 +868,120 @@ describe('client API ', function() { it.skip('should get paginated transaction history', function(done) {}); }); - describe('Export & Import', function() { - var address, importedClient; - beforeEach(function(done) { - importedClient = null; - helpers.createAndJoinWallet(clients, 1, 1, function() { - clients[0].createAddress(function(err, addr) { - should.not.exist(err); - should.exist(addr.address); - address = addr.address; - done(); + describe('Mobility, backup & recovery', function() { + describe('Export & Import', function() { + describe('Success', function() { + var address, importedClient; + beforeEach(function(done) { + importedClient = null; + helpers.createAndJoinWallet(clients, 1, 1, function() { + clients[0].createAddress(function(err, addr) { + should.not.exist(err); + should.exist(addr.address); + address = addr.address; + done(); + }); + }); + }); + afterEach(function(done) { + importedClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list.length.should.equal(1); + list[0].address.should.equal(address); + done(); + }); }); - }); - }); - afterEach(function(done) { - importedClient.getMainAddresses({}, function(err, list) { - should.not.exist(err); - should.exist(list); - list.length.should.equal(1); - list[0].address.should.equal(address); - done(); - }); - }); - it('should export & import', function() { - var exported = clients[0].export(); + it('should export & import', function() { + var exported = clients[0].export(); - importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported); - }); - it.skip('should export & import compressed', function() { - var walletId = clients[0].credentials.walletId; - var walletName = clients[0].credentials.walletName; - var copayerName = clients[0].credentials.copayerName; + importedClient = helpers.newClient(app); + importedClient.import(exported); + }); + it.skip('should export & import compressed', function() { + var walletId = clients[0].credentials.walletId; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; - var exported = clients[0].export({ - compressed: true - }); + var exported = clients[0].export({ + compressed: true + }); - importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported, { - compressed: true - }); - importedClient.credentials.walletId.should.equal(walletId); - importedClient.credentials.walletName.should.equal(walletName); - importedClient.credentials.copayerName.should.equal(copayerName); - }); - it('should export & import encrypted', function() { - var xPrivKey = clients[0].credentials.xPrivKey; - should.exist(xPrivKey); + importedClient = helpers.newClient(app); + importedClient.import(exported, { + compressed: true + }); + importedClient.credentials.walletId.should.equal(walletId); + importedClient.credentials.walletName.should.equal(walletName); + importedClient.credentials.copayerName.should.equal(copayerName); + }); + it('should export & import encrypted', function() { + var xPrivKey = clients[0].credentials.xPrivKey; + should.exist(xPrivKey); - var exported = clients[0].export({ - password: '123' - }); - exported.should.not.contain(xPrivKey); + var exported = clients[0].export({ + password: '123' + }); + exported.should.not.contain(xPrivKey); - importedClient = new Client({ - request: helpers.getRequest(app), + importedClient = helpers.newClient(app); + importedClient.import(exported, { + password: '123' + }); + should.exist(importedClient.credentials.xPrivKey); + importedClient.credentials.xPrivKey.should.equal(xPrivKey); + }); + it('should export & import compressed & encrypted', function() { + var exported = clients[0].export({ + compressed: true, + password: '123' + }); + + importedClient = helpers.newClient(app); + importedClient.import(exported, { + compressed: true, + password: '123' + }); + }); }); - importedClient.import(exported, { - password: '123' + describe('Fail', function() { + it.skip('should fail to export compressed & import uncompressed', function() {}); + it.skip('should fail to export uncompressed & import compressed', function() {}); + it.skip('should fail to export unencrypted & import with password', function() {}); + it.skip('should fail to export encrypted & import with incorrect password', function() {}); }); - should.exist(importedClient.credentials.xPrivKey); - importedClient.credentials.xPrivKey.should.equal(xPrivKey); }); - it('should export & import compressed & encrypted', function() { - var exported = clients[0].export({ - compressed: true, - password: '123' - }); - importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported, { - compressed: true, - password: '123' + describe('Recovery', function() { + it('should be able to regain access to a 1-1 wallet with just the xPriv', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { + var xpriv = clients[0].credentials.xPrivKey; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; + + clients[0].createAddress(function(err, addr) { + should.not.exist(err); + should.exist(addr); + + var recoveryClient = helpers.newClient(app); + recoveryClient.seedFromExtendedPrivateKey(xpriv); + recoveryClient.openWallet(function(err) { + console.log(err); + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); + }); + }); + }); + }); }); }); - it.skip('should fail to export compressed & import uncompressed', function() {}); - it.skip('should fail to export uncompressed & import compressed', function() {}); - it.skip('should fail to export unencrypted & import with password', function() {}); - it.skip('should fail to export encrypted & import with incorrect password', function() {}); }); describe('Air gapped related flows', function() { @@ -960,9 +991,7 @@ describe('client API ', function() { }); var seed = airgapped.getSeed(); - var proxy = new Client({ - request: helpers.getRequest(app), - }); + var proxy = helpers.newClient(app); proxy.seedFromAirGapped(seed); should.not.exist(proxy.credentials.xPrivKey); proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { @@ -981,9 +1010,7 @@ describe('client API ', function() { }); var seed = airgapped.getSeed(); - var proxy = new Client({ - request: helpers.getRequest(app), - }); + var proxy = helpers.newClient(app); proxy.seedFromAirGapped(seed); async.waterfall([ From f89c863419d59596bef8cadba943b2cbd6194a4c Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 2 Mar 2015 09:39:43 -0300 Subject: [PATCH 13/13] fix bit cmds: address, addresses, balance, broadcast, confirm, history, reject, rm, send, sign, status & txproposals --- bit-wallet/bit-address | 9 +++--- bit-wallet/bit-addresses | 22 +++++++++------ bit-wallet/bit-balance | 9 +++--- bit-wallet/bit-broadcast | 21 ++++++-------- bit-wallet/bit-confirm | 41 ++++++++++++++------------- bit-wallet/bit-history | 57 +++++++++++++++++++------------------- bit-wallet/bit-reject | 21 +++++++------- bit-wallet/bit-rm | 28 ++++++++----------- bit-wallet/bit-send | 20 ++++++------- bit-wallet/bit-sign | 28 +++++++++---------- bit-wallet/bit-status | 2 +- bit-wallet/bit-txproposals | 45 +++++++++--------------------- bit-wallet/cli-utils.js | 6 +++- lib/client/api.js | 2 +- lib/client/credentials.js | 1 + 15 files changed, 150 insertions(+), 162 deletions(-) diff --git a/bit-wallet/bit-address b/bit-wallet/bit-address index 0ffe053..057a25c 100755 --- a/bit-wallet/bit-address +++ b/bit-wallet/bit-address @@ -9,8 +9,9 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -client.createAddress(function(err, x) { - utils.die(err); - console.log('* New Address %s ', x.address); +utils.getClient(program, function (client) { + client.createAddress(function(err, x) { + utils.die(err); + console.log('* New Address %s ', x.address); + }); }); diff --git a/bit-wallet/bit-addresses b/bit-wallet/bit-addresses index 5987e6b..a746030 100755 --- a/bit-wallet/bit-addresses +++ b/bit-wallet/bit-addresses @@ -10,14 +10,20 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -client.getMainAddresses({ - doNotVerify: true -}, function(err, x) { - utils.die(err); - console.log('* Addresses:'); - _.each(x, function(a) { - console.log(' ', a.address); +utils.getClient(program, function (client) { + client.getMainAddresses({ + doNotVerify: true + }, function(err, x) { + utils.die(err); + + if (x.length > 0) { + console.log('* Addresses:'); + _.each(x, function(a) { + console.log(' ', a.address); + }); + } else { + console.log('* No addresses.'); + } }); }); diff --git a/bit-wallet/bit-balance b/bit-wallet/bit-balance index 9f9cc40..c32e8b2 100755 --- a/bit-wallet/bit-balance +++ b/bit-wallet/bit-balance @@ -9,9 +9,10 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -client.getBalance(function(err, x) { - utils.die(err); - console.log('* Wallet balance %s (Locked %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount) ); +utils.getClient(program, function (client) { + client.getBalance(function(err, x) { + utils.die(err); + console.log('* Wallet balance %s (Locked %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount) ); + }); }); diff --git a/bit-wallet/bit-broadcast b/bit-wallet/bit-broadcast index cb94701..a186d19 100755 --- a/bit-wallet/bit-broadcast +++ b/bit-wallet/bit-broadcast @@ -11,19 +11,16 @@ program .parse(process.argv); var args = program.args; -if (!args[0]) - program.help(); +var txpid = args[0] || ''; -var txpid = args[0]; -var client = utils.getClient(program); - -client.getTxProposals({}, function(err, txps) { - utils.die(err); - - var txp = utils.findOneTxProposal(txps, txpid); - client.broadcastTxProposal(txp, function(err, txid) { +utils.getClient(program, function (client) { + client.getTxProposals({}, function(err, txps) { utils.die(err); - console.log('Transaction Broadcasted: TXID: ' + x.txid); - + + var txp = utils.findOneTxProposal(txps, txpid); + client.broadcastTxProposal(txp, function(err, txp) { + utils.die(err); + console.log('Transaction Broadcasted: TXID: ' + txp.txid); + }); }); }); diff --git a/bit-wallet/bit-confirm b/bit-wallet/bit-confirm index 99d4386..9d3002e 100755 --- a/bit-wallet/bit-confirm +++ b/bit-wallet/bit-confirm @@ -9,24 +9,27 @@ program = utils.configureCommander(program); program .parse(process.argv); -var client = utils.getClient(program); - -client.getStatus(function(err, x, myCopayerId) { - utils.die(err); - console.log('\n To be sure that none Copayer has joined more that once to this wallet, you can asked them their confirmation number. They can grab them using this (bit confirm) command.'); - - console.log('\n * Copayer confirmations ids:'); - - var myConfirmationId; - _.each(x.wallet.copayers, function(x) { - var confirmationId = utils.confirmationId(x); - if (x.id != myCopayerId) - console.log('\t\t* %s : %s', x.name, confirmationId); - else - myConfirmationId = confirmationId; +utils.getClient(program, function (client) { + client.getStatus(function(err, x) { + utils.die(err); + + if (x.wallet.n == 1) { + console.log('Confirmations only work on shared wallets'); + process.exit(1); + } + console.log('\n To be sure that no copayer has joined this wallet more than once, you can asked them for their confirmation number. They can get theirs by running the bit-confirm command.'); + console.log('\n * Copayer confirmation IDs:'); + + var myConfirmationId; + _.each(x.wallet.copayers, function(x) { + var confirmationId = utils.confirmationId(x); + if (x.id != client.credentials.copayerId) + console.log('\t\t* %s : %s', x.name, confirmationId); + else + myConfirmationId = confirmationId; + }); + + console.log('\t\t---'); + console.log('\t\tYour confirmation ID: %s', myConfirmationId); }); - - - console.log('\t\t---'); - console.log('\t\tYour confirmation ID: %s', myConfirmationId); }); diff --git a/bit-wallet/bit-history b/bit-wallet/bit-history index 7f250c2..f2bb8db 100755 --- a/bit-wallet/bit-history +++ b/bit-wallet/bit-history @@ -4,40 +4,39 @@ var _ = require('lodash'); var fs = require('fs'); var moment = require('moment'); var program = require('commander'); -var Utils = require('./cli-utils'); -program = Utils.configureCommander(program); +var utils = require('./cli-utils'); +program = utils.configureCommander(program); program .parse(process.argv); var args = program.args; -var client = Utils.getClient(program); -var txData; - -client.getTxHistory({}, function (err, txs) { - if (_.isEmpty(txs)) - return; - - console.log("* TX History:") - - _.each(txs, function(tx) { - var time = moment(tx.time * 1000).fromNow(); - var amount = Utils.renderAmount(tx.amount); - var confirmations = tx.confirmations || 0; - var proposal = tx.proposalId ? '["' + tx.message + '" by ' + tx.creatorName + '] ' : ''; - switch (tx.action) { - case 'received': - direction = '<='; - break; - case 'moved': - direction = '=='; - break; - case 'sent': - direction = '=>'; - break; - } - - console.log("\t%s: %s %s %s %s(%s confirmations)", time, direction, tx.action, amount, proposal, confirmations); +utils.getClient(program, function (client) { + client.getTxHistory({}, function (err, txs) { + if (_.isEmpty(txs)) + return; + + console.log("* TX History:") + + _.each(txs, function(tx) { + var time = moment(tx.time * 1000).fromNow(); + var amount = utils.renderAmount(tx.amount); + var confirmations = tx.confirmations || 0; + var proposal = tx.proposalId ? '["' + tx.message + '" by ' + tx.creatorName + '] ' : ''; + switch (tx.action) { + case 'received': + direction = '<='; + break; + case 'moved': + direction = '=='; + break; + case 'sent': + direction = '=>'; + break; + } + + console.log("\t%s: %s %s %s %s(%s confirmations)", time, direction, tx.action, amount, proposal, confirmations); + }); }); }); diff --git a/bit-wallet/bit-reject b/bit-wallet/bit-reject index de0858c..bdf1fd6 100755 --- a/bit-wallet/bit-reject +++ b/bit-wallet/bit-reject @@ -13,17 +13,18 @@ program var args = program.args; var txpid = args[0] || ''; var reason = args[1] || ''; -var client = utils.getClient(program); -client.getTxProposals({}, function(err, txps) { - utils.die(err); - - var txp = utils.findOneTxProposal(txps, txpid); - client.rejectTxProposal(txp, reason, function(err, tx) { +utils.getClient(program, function (client) { + client.getTxProposals({}, function(err, txps) { utils.die(err); - if (tx.status == 'rejected') - console.log('Transaction finally rejected.'); - else - console.log('Transaction rejected by you.'); + + var txp = utils.findOneTxProposal(txps, txpid); + client.rejectTxProposal(txp, reason, function(err, tx) { + utils.die(err); + if (tx.status == 'rejected') + console.log('Transaction finally rejected.'); + else + console.log('Transaction rejected by you.'); + }); }); }); diff --git a/bit-wallet/bit-rm b/bit-wallet/bit-rm index a27a787..b0472aa 100755 --- a/bit-wallet/bit-rm +++ b/bit-wallet/bit-rm @@ -12,26 +12,20 @@ program .parse(process.argv); var args = program.args; -if (!args[0]) - program.help(); +var txpid = args[0] || ''; -var txpid = args[0]; - -var cli = new Client({ - filename: program.config -}); - -cli.getTxProposals({}, function(err, txps) { - utils.die(err); - - if (program.verbose) - console.log('* Raw Server Response:\n', txps); //TODO +utils.getClient(program, function (client) { + client.getTxProposals({}, function(err, txps) { + utils.die(err); - var txp = utils.findOneTxProposal(txps, txpid); + if (program.verbose) + console.log('* Raw Server Response:\n', txps); //TODO - cli.removeTxProposal(txp, function(err) { - utils.die(err); + var txp = utils.findOneTxProposal(txps, txpid); + client.removeTxProposal(txp, function(err) { + utils.die(err); - console.log('Transaction removed.'); + console.log('Transaction removed.'); + }); }); }); diff --git a/bit-wallet/bit-send b/bit-wallet/bit-send index b8b8194..780eba5 100755 --- a/bit-wallet/bit-send +++ b/bit-wallet/bit-send @@ -31,14 +31,14 @@ try { } var note = args[2]; -var client = utils.getClient(program); - -client.sendTxProposal({ - toAddress: address, - amount: amount, - message: note -}, function(err, x) { - utils.die(err); - console.log(' * Tx created: ID %s [%s] RequiredSignatures:', - x.id, x.status, x.requiredSignatures); +utils.getClient(program, function (client) { + client.sendTxProposal({ + toAddress: address, + amount: amount, + message: note + }, function(err, x) { + utils.die(err); + console.log(' * Tx created: ID %s [%s] RequiredSignatures:', + x.id, x.status, x.requiredSignatures); + }); }); diff --git a/bit-wallet/bit-sign b/bit-wallet/bit-sign index 9faad47..b11c32d 100755 --- a/bit-wallet/bit-sign +++ b/bit-wallet/bit-sign @@ -15,9 +15,7 @@ program var args = program.args; var txpid = args[0] || ''; -var client = utils.getClient(program); - -function end(txp) { +function end(client, txp) { if (program.output) { client.getSignatures(txp, function(err, signatures) { utils.die(err); @@ -50,14 +48,16 @@ function end(txp) { }; -if (program.input && program.output) { - var inFile = JSON.parse(fs.readFileSync(program.input)); - end(inFile.txps[0]); -} else { - client.getTxProposals({}, function(err, txps) { - utils.die(err); - var txp = utils.findOneTxProposal(txps, txpid); - utils.die(err); - end(txp); - }); -} +utils.getClient(program, function (client) { + if (program.input && program.output) { + var inFile = JSON.parse(fs.readFileSync(program.input)); + end(client, inFile.txps[0]); + } else { + client.getTxProposals({}, function(err, txps) { + utils.die(err); + var txp = utils.findOneTxProposal(txps, txpid); + utils.die(err); + end(client, txp); + }); + } +}); diff --git a/bit-wallet/bit-status b/bit-wallet/bit-status index 37bd1e2..4f29ce4 100755 --- a/bit-wallet/bit-status +++ b/bit-wallet/bit-status @@ -14,7 +14,7 @@ utils.getClient(program, function (client) { utils.die(err); var x = res.wallet; - console.log('* Wallet %s [%s]: %d-%d %s ', x.name, x.network, x.m, x.n, x.status); + console.log('* Wallet %s [%s]: %d-of-%d %s ', x.name, x.network, x.m, x.n, x.status); if (x.status != 'complete') console.log(' Missing copayers:', x.n - x.copayers.length); diff --git a/bit-wallet/bit-txproposals b/bit-wallet/bit-txproposals index 26a27ae..2bcef75 100755 --- a/bit-wallet/bit-txproposals +++ b/bit-wallet/bit-txproposals @@ -7,39 +7,20 @@ var utils = require('./cli-utils'); program = utils.configureCommander(program); program - .option('-i, --input [filename]', 'use input file instead of server\'s') - .option('-o, --output [filename]', 'write tx to output file') + .option('-o, --output [filename]', 'write tx to output file for offline signing') .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -var txData; - -function end(err, txps, rawtxps) { - utils.die(err); - if (program.input) { - console.log('\n* From File : %s\n', program.input); - } - utils.renderTxProposals(txps); - if (program.output) { - - client.getEncryptedWalletData(function (err, toComplete) { - var txData = { - toComplete: toComplete, - txps: txps, - }; - fs.writeFileSync(program.output, JSON.stringify(txData)); - console.log(' * Proposals Saved to: %s\n', program.output); - }); - } -}; - - -if (program.input) { - var txData = fs.readFileSync(program.input); - txData = JSON.parse(txData); - client.parseTxProposals(txData, end); -} else { - client.getTxProposals({getRawTxps: !!program.output}, end); -} +utils.getClient(program, function (client) { + client.getTxProposals({forAirGapped: !!program.output}, function (err, res) { + utils.die(err); + + if (program.output) { + fs.writeFileSync(program.output, JSON.stringify(res)); + console.log(' * Tx proposals saved to: %s\n', program.output); + } else { + utils.renderTxProposals(res); + } + }); +}); diff --git a/bit-wallet/cli-utils.js b/bit-wallet/cli-utils.js index 4613a19..b621c24 100644 --- a/bit-wallet/cli-utils.js +++ b/bit-wallet/cli-utils.js @@ -195,7 +195,11 @@ Utils.renderTxProposals = function(txps) { return a.copayerName + ' ' + (a.type == 'accept' ? '✓' : '✗') + (a.comment ? ' (' + a.comment + ')' : ''); }).join('. ')); } - console.log('\t\tMissing signatures: ' + missingSignatures); + if (missingSignatures > 0) { + console.log('\t\tMissing signatures: ' + missingSignatures); + } else { + console.log('\t\tReady to broadcast'); + } }); }; diff --git a/lib/client/api.js b/lib/client/api.js index fc73851..82fbcee 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -344,7 +344,7 @@ API.prototype.getStatus = function(cb) { self._doGetRequest('/v1/wallets/', function(err, result) { _processTxps(result.pendingTxps, self.credentials.sharedEncryptingKey); - return cb(err, result, self.credentials.copayerId); + return cb(err, result); }); }; diff --git a/lib/client/credentials.js b/lib/client/credentials.js index 29f9373..becf254 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -17,6 +17,7 @@ var FIELDS = [ 'm', 'n', 'walletPrivKey', + 'personalEncryptingKey', 'sharedEncryptingKey', 'copayerName', ];