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);