diff --git a/bit-wallet/bit-join b/bit-wallet/bit-join index d30b63a..bbfb314 100755 --- a/bit-wallet/bit-join +++ b/bit-wallet/bit-join @@ -18,7 +18,7 @@ var secret = args[0]; var copayerName = args[1] || process.env.USER; var client = utils.getClient(program); -cli.joinWallet(secret, copayerName, function(err, xx) { +client.joinWallet(secret, copayerName, function(err, xx) { utils.die(err); console.log(' * Wallet Joined.', xx || ''); }); diff --git a/bit-wallet/bit-reject b/bit-wallet/bit-reject old mode 100644 new mode 100755 diff --git a/bit-wallet/bit-rm b/bit-wallet/bit-rm old mode 100644 new mode 100755 diff --git a/bit-wallet/bit-status b/bit-wallet/bit-status index 6ff6230..32fa992 100755 --- a/bit-wallet/bit-status +++ b/bit-wallet/bit-status @@ -17,7 +17,7 @@ client.getStatus(function(err, res) { utils.die(err); var x = res.wallet; - console.log('* Wallet %s [%s]: %d-%d %s ', x.name, x.isTestnet ? 'testnet' : 'livenet', x.m, x.n, x.status); + console.log('* Wallet %s [%s]: %d-%d %s ', x.name, x.network, x.m, x.n, x.status); var x = res.balance; console.log('* Balance %d (Locked: %d)', x.totalAmount, x.lockedAmount); diff --git a/lib/bitcoinutils.js b/lib/bitcoinutils.js new file mode 100644 index 0000000..2a29ea1 --- /dev/null +++ b/lib/bitcoinutils.js @@ -0,0 +1,25 @@ + +var _ = require('lodash'); + +var Bitcore = require('bitcore'); +var BitcoreAddress = Bitcore.Address; + +function BitcoinUtils () {}; + +BitcoinUtils.deriveAddress = function(publicKeyRing, path, m, network) { + + var publicKeys = _.map(publicKeyRing, function(xPubKey) { + var xpub = new Bitcore.HDPublicKey(xPubKey); + return xpub.derive(path).publicKey; + }); + + var bitcoreAddress = BitcoreAddress.createMultisig(publicKeys, m, network); + + return { + address: bitcoreAddress.toString(), + path: path, + publicKeys: _.invoke(publicKeys, 'toString'), + }; +}; + +module.exports = BitcoinUtils; diff --git a/lib/client/API.js b/lib/client/API.js deleted file mode 100644 index f582925..0000000 --- a/lib/client/API.js +++ /dev/null @@ -1,382 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var util = require('util'); -var async = require('async'); -var log = require('npmlog'); -var request = require('request') -log.debug = log.verbose; - -var Bitcore = require('bitcore') -var SignUtils = require('../signutils'); - -var BASE_URL = 'http://localhost:3001/copay/api'; - -function _createProposalOpts(opts, signingKey) { - var msg = opts.toAddress + '|' + opts.amount + '|' + opts.message; - opts.proposalSignature = SignUtils.sign(msg, signingKey); - return opts; -}; - -function _getUrl(path) { - return BASE_URL + path; -}; - -function _parseError(body) { - if (_.isString(body)) { - try { - body = JSON.parse(body); - } catch (e) { - body = { - error: body - }; - } - } - var code = body.code || 'ERROR'; - var message = body.error || 'There was an unknown error processing the request'; - log.error(code, message); -}; - -function _signRequest(method, url, args, privKey) { - var message = method.toLowerCase() + '|' + url + '|' + JSON.stringify(args); - return SignUtils.sign(message, privKey); -}; - -function _createXPrivKey(network) { - return new Bitcore.HDPrivateKey(network).toString(); -}; - -function API(opts) { - if (!opts.storage) { - throw new Error('Must provide storage option'); - } - this.storage = opts.storage; - this.verbose = !!opts.verbose; - if (this.verbose) { - log.level = 'debug'; - } -}; - - -API.prototype._loadAndCheck = function() { - var data = this.storage.load(); - if (!data) { - log.error('Wallet file not found.'); - process.exit(1); - } - - if (data.verified == 'corrupt') { - log.error('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); - process.exit(1); - } - if (data.n > 1) { - var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; - if (!pkrComplete) { - log.warn('The file ' + this.filename + ' is incomplete. It will allow you to operate with the wallet but it should not be trusted as a backup. Please wait for all copayers to join the wallet and run the tool with -export flag.') - } - } - return data; -}; - -API.prototype._doRequest = function(method, url, args, data, cb) { - var reqSignature = _signRequest(method, url, args, data.signingPrivKey); - var absUrl = _getUrl(url); - var args = { - headers: { - 'x-identity': data.copayerId, - 'x-signature': reqSignature, - }, - method: method, - url: absUrl, - body: args, - json: true, - }; - log.verbose('Request Args', util.inspect(args)); - request(args, function(err, res, body) { - log.verbose('Response:', err, body); - - if (err) return cb(err); - if (res.statusCode != 200) { - _parseError(body); - return cb('Request error'); - } - return cb(null, body); - }); -}; - - -API.prototype._doPostRequest = function(url, args, data, cb) { - return this._doRequest('post', url, args, data, cb); -}; - -API.prototype._doGetRequest = function(url, data, cb) { - return this._doRequest('get', url, {}, data, cb); -}; - - - -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'); - - var data = this.storage.load(); - if (data) return cb('File ' + this.filename + ' already contains a wallet'); - - // Generate wallet key pair to verify copayers - var privKey = new Bitcore.PrivateKey(null, network); - var pubKey = privKey.toPublicKey(); - - data = { - m: m, - n: n, - walletPrivKey: privKey.toString(), - }; - - var args = { - name: walletName, - m: m, - n: n, - pubKey: pubKey.toString(), - network: network, - }; - var url = '/v1/wallets/'; - - this._doPostRequest(url, args, data, function(err, body) { - if (err) return cb(err); - - var walletId = body.walletId; - var secret = walletId + ':' + privKey.toString() + ':' + (network == 'testnet' ? 'T' : 'L'); - var ret; - - if (n > 1) - ret = data.secret = secret; - - self.storage.save(data); - self._joinWallet(data, secret, copayerName, function(err) { - if (err) return cb(err); - - return cb(null, ret); - }); - }); -}; - -API.prototype._joinWallet = function(data, secret, copayerName, cb) { - var self = this; - data = data || {}; - - var secretSplit = secret.split(':'); - var walletId = secretSplit[0]; - - var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]); - var network = secretSplit[2] == 'T' ? 'testnet' : 'livenet'; - data.xPrivKey = _createXPrivKey(network); - - var xPubKey = new Bitcore.HDPublicKey(data.xPrivKey); - var xPubKeySignature = SignUtils.sign(xPubKey.toString(), walletPrivKey); - - var signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey; - var args = { - walletId: walletId, - name: copayerName, - xPubKey: xPubKey.toString(), - xPubKeySignature: xPubKeySignature, - }; - var url = '/v1/wallets/' + walletId + '/copayers'; - - this._doPostRequest(url, args, data, function(err, body) { - var wallet = body.wallet; - data.copayerId = body.copayerId; - data.walletPrivKey = walletPrivKey; - data.signingPrivKey = signingPrivKey.toString(); - data.m = wallet.m; - data.n = wallet.n; - data.publicKeyRing = wallet.publicKeyRing; - self.storage.save(data); - - return cb(); - }); -}; - -API.prototype.joinWallet = function(secret, copayerName, cb) { - var self = this; - - var data = this.storage.load(); - if (data) return cb('File ' + this.filename + ' already contains a wallet'); - - self._joinWallet(data, secret, copayerName, cb); -}; - -API.prototype.getStatus = function(cb) { - var self = this; - - var data = this._loadAndCheck(); - - var url = '/v1/wallets/'; - this._doGetRequest(url, data, function(err, body) { - if (err) return cb(err); - - var wallet = body; - if (wallet.n > 0 && wallet.status === 'complete' && !data.verified) { - var pubKey = Bitcore.PrivateKey.fromString(data.walletPrivKey).toPublicKey().toString(); - var fake = []; - _.each(wallet.copayers, function(copayer) { - - - console.log('[clilib.js.224]', copayer.xPubKey, copayer.xPubKeySignature, pubKey); //TODO - if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { - - console.log('[clilib.js.227] FAKE'); //TODO - fake.push(copayer); - } - }); - if (fake.length > 0) { - log.error('Some copayers in the wallet could not be verified to have known the wallet secret'); - data.verified = 'corrupt'; - } else { - data.verified = 'ok'; - } - self.storage.save(data); - } - - return cb(null, wallet); - }); -}; - -/** - * send - * - * @param inArgs - * @param inArgs.toAddress - * @param inArgs.amount - * @param inArgs.message - */ -API.prototype.sendTxProposal = function(inArgs, cb) { - var self = this; - - var data = this._loadAndCheck(); - var args = _createProposalOpts(inArgs, data.signingPrivKey); - - var url = '/v1/txproposals/'; - this._doPostRequest(url, args, data, cb); -}; - -// Get addresses -API.prototype.getAddresses = function(cb) { - var self = this; - - var data = this._loadAndCheck(); - - var url = '/v1/addresses/'; - this._doGetRequest(url, data, cb); -}; - - -// Creates a new address -// TODO: verify derivation!! -API.prototype.createAddress = function(cb) { - var self = this; - - var data = this._loadAndCheck(); - - var url = '/v1/addresses/'; - this._doPostRequest(url, {}, data, cb); -}; - -API.prototype.history = function(limit, cb) { - -}; - -API.prototype.getBalance = function(cb) { - var self = this; - - var data = this._loadAndCheck(); - - var url = '/v1/balance/'; - this._doGetRequest(url, data, cb); -}; - - -API.prototype.getTxProposals = function(opts, cb) { - var self = this; - - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/'; - this._doGetRequest(url, data, cb); -}; - -API.prototype.signTxProposal = function(txp, cb) { - var self = this; - var data = this._loadAndCheck(); - - - //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(data.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) - .sign(privs); - - var signatures = []; - _.each(privs, function(p) { - var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); - signatures.push(s); - }); - - var url = '/v1/txproposals/' + txp.id + '/signatures/'; - var args = { - signatures: signatures - }; - - this._doPostRequest(url, args, data, cb); -}; - -API.prototype.rejectTxProposal = function(txp, reason, cb) { - var self = this; - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/' + txp.id + '/rejections/'; - var args = { - reason: reason || '', - }; - this._doPostRequest(url, args, data, cb); -}; - -API.prototype.broadcastTxProposal = function(txp, cb) { - var self = this; - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - this._doPostRequest(url, {}, data, cb); -}; - - - -API.prototype.removeTxProposal = function(txp, cb) { - var self = this; - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/' + txp.id; - - this._doRequest('delete', url, {}, data, cb); -}; - -module.exports = API; diff --git a/lib/client/FileStorage.js b/lib/client/FileStorage.js deleted file mode 100644 index 27fe3d4..0000000 --- a/lib/client/FileStorage.js +++ /dev/null @@ -1,24 +0,0 @@ - -var fs = require('fs') - -function FileStorage(opts) { - if (!opts.filename) { - throw new Error('Please set the config filename'); - } - this.filename = opts.filename; -}; - - -FileStorage.prototype.save = function(data) { - fs.writeFileSync(this.filename, JSON.stringify(data)); -}; - -FileStorage.prototype.load = function() { - try { - return JSON.parse(fs.readFileSync(this.filename)); - } catch (ex) {} -}; - - -module.exports = FileStorage; - diff --git a/lib/client/Verifier.js b/lib/client/Verifier.js new file mode 100644 index 0000000..42ae592 --- /dev/null +++ b/lib/client/Verifier.js @@ -0,0 +1,59 @@ +var $ = require('preconditions').singleton(); +var _ = require('lodash'); +var log = require('npmlog'); + +var Bitcore = require('bitcore'); +var BitcoinUtils = require('../bitcoinutils') +var SignUtils = require('../signutils'); + +/* + * Checks data given by the server + */ + +function Verifier(opts) {}; + +Verifier.checkAddress = function(data, address) { + var local = BitcoinUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); + return (local.address == address.address && JSON.stringify(local.publicKeys) == JSON.stringify(address.publicKeys)); +}; + + +// + +Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { + + var walletPubKey = Bitcore.PrivateKey.fromString(walletPrivKey).toPublicKey().toString(); + + if (copayers.length != n) { + log.error('Missing public keys in server response'); + return false; + } + + // Repeated xpub kes? + var uniq = []; + var error; + _.each(copayers, function(copayer) { + if (uniq[copayers.xPubKey]++) { + log.error('Repeated public keys in server response'); + error = true; + } + + // Not signed pub keys + if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) { + log.error('Invalid signatures in server response'); + error = true; + } + }); + if (error) + return false; + + var myXPubKey = new Bitcore.HDPublicKey(myXPrivKey).toString(); + if (!_.contains(_.pluck(copayers, 'xPubKey'), myXPubKey)) { + log.error('Server response does not contains our public keys') + return false; + } + return true; +}; + + +module.exports = Verifier; diff --git a/lib/client/api.js b/lib/client/api.js new file mode 100644 index 0000000..066308d --- /dev/null +++ b/lib/client/api.js @@ -0,0 +1,410 @@ +'use strict'; + +var _ = require('lodash'); +var $ = require('preconditions').singleton(); +var util = require('util'); +var async = require('async'); +var log = require('npmlog'); +var request = require('request') +log.debug = log.verbose; + +var Bitcore = require('bitcore') +var SignUtils = require('../signutils'); +var Verifier = require('./verifier'); +var ServerCompromisedError = require('./servercompromisederror') + +var BASE_URL = 'http://localhost:3001/copay/api'; + +function _createProposalOpts(opts, signingKey) { + var msg = opts.toAddress + '|' + opts.amount + '|' + opts.message; + opts.proposalSignature = SignUtils.sign(msg, signingKey); + return opts; +}; + +function _getUrl(path) { + return BASE_URL + path; +}; + +function _parseError(body) { + if (_.isString(body)) { + try { + body = JSON.parse(body); + } catch (e) { + body = { + error: body + }; + } + } + var code = body.code || 'ERROR'; + var message = body.error || 'There was an unknown error processing the request'; + log.error(code, message); +}; + +function _signRequest(method, url, args, privKey) { + var message = method.toLowerCase() + '|' + url + '|' + JSON.stringify(args); + return SignUtils.sign(message, privKey); +}; + +function _createXPrivKey(network) { + return new Bitcore.HDPrivateKey(network).toString(); +}; + +function API(opts) { + if (!opts.storage) { + throw new Error('Must provide storage option'); + } + this.storage = opts.storage; + this.verbose = !!opts.verbose; + this.request = request || opts.request; + if (this.verbose) { + log.level = 'debug'; + } +}; + + + +API.prototype._tryToComplete = function(data, cb) { + var self = this; + + var url = '/v1/wallets/'; + self._doGetRequest(url, data, 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, data.walletPrivKey, data.xPrivKey, data.n)) + return cb('Some copayers in the wallet could not be verified to have known the wallet secret'); + + data.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') + + self.storage.save(data, function(err) { + return cb(err, data); + }); + }); +}; + + +API.prototype._loadAndCheck = function(cb) { + var self = this; + + this.storage.load(function(err, data) { + if (err || !data) { + return cb(err || 'Wallet file not found.'); + } + if (data.n > 1) { + var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; + + if (!pkrComplete) { + return self._tryToComplete(data, cb); + } + } + return cb(null, data); + }); +}; + +API.prototype._doRequest = function(method, url, args, data, cb) { + var reqSignature = _signRequest(method, url, args, data.signingPrivKey); + var absUrl = _getUrl(url); + var args = { + headers: { + 'x-identity': data.copayerId, + 'x-signature': reqSignature, + }, + method: method, + url: absUrl, + body: args, + json: true, + }; + log.verbose('Request Args', util.inspect(args, { + depth: 10 + })); + this.request(args, function(err, res, body) { + log.verbose(util.inspect(body, { + depth: 10 + })); + if (err) return cb(err); + if (res.statusCode != 200) { + _parseError(body); + return cb('Request error'); + } + + return cb(null, body); + }); +}; + + +API.prototype._doPostRequest = function(url, args, data, cb) { + return this._doRequest('post', url, args, data, cb); +}; + +API.prototype._doGetRequest = function(url, data, cb) { + return this._doRequest('get', url, {}, data, cb); +}; + + + +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.storage.load(function(err, data) { + if (data) + return cb('Storage already contains a wallet'); + + console.log('[API.js.132]'); //TODO + // Generate wallet key pair to verify copayers + var privKey = new Bitcore.PrivateKey(null, network); + var pubKey = privKey.toPublicKey(); + + data = { + m: m, + n: n, + walletPrivKey: privKey.toWIF(), + network: network, + }; + + var args = { + name: walletName, + m: m, + n: n, + pubKey: pubKey.toString(), + network: network, + }; + var url = '/v1/wallets/'; + + self._doPostRequest(url, args, data, function(err, body) { + if (err) return cb(err); + + var walletId = body.walletId; + var secret = walletId + ':' + privKey.toString() + ':' + (network == 'testnet' ? 'T' : 'L'); + var ret; + + if (n > 1) + ret = data.secret = secret; + + self.storage.save(data, function(err) { + if (err) return cb(err); + self._joinWallet(data, secret, copayerName, function(err) { + return cb(err, ret); + }); + + }); + }); + }); +}; + +API.prototype._joinWallet = function(data, secret, copayerName, cb) { + var self = this; + data = data || {}; + + var secretSplit = secret.split(':'); + var walletId = secretSplit[0]; + + var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]); + var network = secretSplit[2] == 'T' ? 'testnet' : 'livenet'; + data.xPrivKey = _createXPrivKey(network); + + var xPubKey = new Bitcore.HDPublicKey(data.xPrivKey); + var xPubKeySignature = SignUtils.sign(xPubKey.toString(), walletPrivKey); + + var signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey; + var args = { + walletId: walletId, + name: copayerName, + xPubKey: xPubKey.toString(), + xPubKeySignature: xPubKeySignature, + }; + var url = '/v1/wallets/' + walletId + '/copayers'; + + this._doPostRequest(url, args, data, function(err, body) { + var wallet = body.wallet; + data.copayerId = body.copayerId; + data.walletPrivKey = walletPrivKey.toWIF(); + data.signingPrivKey = signingPrivKey.toString(); + data.m = wallet.m; + data.n = wallet.n; + data.publicKeyRing = wallet.publicKeyRing; + data.network = wallet.network, + self.storage.save(data, cb); + }); +}; + +API.prototype.joinWallet = function(secret, copayerName, cb) { + var self = this; + + this.storage.load(function(err, data) { + if (data) + return cb('Storage already contains a wallet'); + + self._joinWallet(data, secret, copayerName, cb); + }); +}; + +API.prototype.getStatus = function(cb) { + var self = this; + + this._loadAndCheck(function(err, data) { + if (err) return cb(err); + + var url = '/v1/wallets/'; + self._doGetRequest(url, data, function(err, body) { + return cb(err, body); + }); + }); +}; + +/** + * send + * + * @param inArgs + * @param inArgs.toAddress + * @param inArgs.amount + * @param inArgs.message + */ +API.prototype.sendTxProposal = function(inArgs, cb) { + var self = this; + + this._loadAndCheck(function(err, data) { + if (err) return cb(err); + + var args = _createProposalOpts(inArgs, data.signingPrivKey); + + var url = '/v1/txproposals/'; + self._doPostRequest(url, args, data, cb); + }); +}; + +API.prototype.createAddress = function(cb) { + var self = this; + + this._loadAndCheck(function(err, data) { + if (err) return cb(err); + + var url = '/v1/addresses/'; + self._doPostRequest(url, {}, data, function(err, address) { + if (err) return cb(err); + if (!Verifier.checkAddress(data, address)) { + return cb(new ServerCompromisedError('Server sent fake address')); + } + + return cb(null, address); + }); + }); +}; + +API.prototype.history = function(limit, cb) { + +}; + +API.prototype.getBalance = function(cb) { + var self = this; + + this._loadAndCheck(function(err, data) { + if (err) return cb(err); + var url = '/v1/balance/'; + self._doGetRequest(url, data, cb); + }); +}; + + +API.prototype.getTxProposals = function(opts, cb) { + var self = this; + + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); + var url = '/v1/txproposals/'; + self._doGetRequest(url, data, cb); + }); +}; + +API.prototype.signTxProposal = function(txp, cb) { + var self = this; + + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); + + + //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(data.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) + .sign(privs); + + var signatures = []; + _.each(privs, function(p) { + var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); + signatures.push(s); + }); + + var url = '/v1/txproposals/' + txp.id + '/signatures/'; + var args = { + signatures: signatures + }; + + self._doPostRequest(url, args, data, cb); + }); +}; + +API.prototype.rejectTxProposal = function(txp, reason, cb) { + var self = this; + + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); + + var url = '/v1/txproposals/' + txp.id + '/rejections/'; + var args = { + reason: reason || '', + }; + self._doPostRequest(url, args, data, cb); + }); +}; + +API.prototype.broadcastTxProposal = function(txp, cb) { + var self = this; + + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); + + var url = '/v1/txproposals/' + txp.id + '/broadcast/'; + self._doPostRequest(url, {}, data, cb); + }); +}; + + + +API.prototype.removeTxProposal = function(txp, cb) { + var self = this; + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); + var url = '/v1/txproposals/' + txp.id; + self._doRequest('delete', url, {}, data, cb); + }); +}; + +module.exports = API; diff --git a/lib/client/filestorage.js b/lib/client/filestorage.js new file mode 100644 index 0000000..35e6e69 --- /dev/null +++ b/lib/client/filestorage.js @@ -0,0 +1,30 @@ + +var fs = require('fs') + +function FileStorage(opts) { + if (!opts.filename) { + throw new Error('Please set the config filename'); + } + this.filename = opts.filename; + this.fs = opts.fs || fs; +}; + + +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 24eb668..fd416f1 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1,8 +1,5 @@ //var client = ; -var client = module.exports = require('./API'); -client.FileStorage = require('./FileStorage'); - - -// TODO -//module.exports.storage = require('./storage'); +var client = module.exports = require('./api'); +client.FileStorage = require('./filestorage'); +client.Verifier = require('./verifier'); diff --git a/lib/client/servercompromisederror.js b/lib/client/servercompromisederror.js new file mode 100644 index 0000000..7c4df64 --- /dev/null +++ b/lib/client/servercompromisederror.js @@ -0,0 +1,6 @@ +function ServerCompromisedError(message) { + this.code = 'SERVERCOMPROMISED'; + this.message = message; +}; + +module.exports = ServerCompromisedError; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index 80b4ed4..6c91cdd 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -3,14 +3,12 @@ var _ = require('lodash'); var util = require('util'); var $ = require('preconditions').singleton(); - -var Bitcore = require('bitcore'); -var BitcoreAddress = Bitcore.Address; var Uuid = require('uuid'); var Address = require('./address'); var Copayer = require('./copayer'); var AddressManager = require('./addressmanager'); +var BitcoinUtils = require('../bitcoinutils'); var VERSION = '1.0.0'; @@ -28,7 +26,7 @@ function Wallet(opts) { this.addressIndex = 0; this.copayers = []; this.pubKey = opts.pubKey; - this.isTestnet = opts.isTestnet; + this.network = opts.network; this.addressManager = new AddressManager(); }; @@ -76,7 +74,7 @@ Wallet.fromObj = function(obj) { return Copayer.fromObj(copayer); }); x.pubKey = obj.pubKey; - x.isTestnet = obj.isTestnet; + x.network = obj.network; x.addressManager = AddressManager.fromObj(obj.addressManager); return x; @@ -101,13 +99,8 @@ Wallet.prototype.getCopayer = function(copayerId) { }); }; - Wallet.prototype.getNetworkName = function() { - return this.isTestnet ? 'testnet' : 'livenet'; -}; - -Wallet.prototype._getBitcoreNetwork = function() { - return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet; + return this.network; }; @@ -124,19 +117,7 @@ Wallet.prototype.createAddress = function(isChange) { $.checkState(this.isComplete()); var path = this.addressManager.getNewAddressPath(isChange); - - var publicKeys = _.map(this.copayers, function(copayer) { - var xpub = new Bitcore.HDPublicKey(copayer.xPubKey); - return xpub.derive(path).publicKey; - }); - - var bitcoreAddress = BitcoreAddress.createMultisig(publicKeys, this.m, this._getBitcoreNetwork()); - - return new Address({ - address: bitcoreAddress.toString(), - path: path, - publicKeys: _.invoke(publicKeys, 'toString'), - }); + return new Address(BitcoinUtils.deriveAddress(this.publicKeyRing, path, this.m, this.network)); }; diff --git a/lib/server.js b/lib/server.js index 66b1985..ac1373d 100644 --- a/lib/server.js +++ b/lib/server.js @@ -118,7 +118,7 @@ CopayServer.prototype.createWallet = function(opts, cb) { name: opts.name, m: opts.m, n: opts.n, - isTestnet: network === 'testnet', + network: network, pubKey: pubKey, }); @@ -669,7 +669,6 @@ CopayServer.prototype.signTx = function(opts, cb) { txProposalId: opts.txProposalId, txid: txid }); - return cb(null, txp); }); }); diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js new file mode 100644 index 0000000..07f29fd --- /dev/null +++ b/test/integration/clientApi.js @@ -0,0 +1,153 @@ +'use strict'; + +var _ = require('lodash'); +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var Client = require('../../lib/client'); +var API = Client.API; +var Bitcore = require('bitcore'); +var TestData = require('./clienttestdata'); + +describe(' client API ', function() { + + var client; + + beforeEach(function() { + + var fsmock = {};; + fsmock.readFile = sinon.mock().yields(null, JSON.stringify(TestData.storage.wallet11)); + fsmock.writeFile = sinon.mock().yields(); + var storage = new Client.FileStorage({ + filename: 'dummy', + fs: fsmock, + }); + client = new Client({ + storage: storage + }); + }); + + describe(' _tryToComplete ', function() { + it('should complete a wallet ', function(done) { + var request = sinon.stub(); + + // Wallet request + request.onCall(0).yields(null, { + statusCode: 200, + }, TestData.serverResponse.completeWallet); + request.onCall(1).yields(null, { + statusCode: 200, + }, "pepe"); + + client.request = request; + client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); + client.getBalance(function(err, x) { + should.not.exist(err); + done(); + }); + }) + + + it('should handle incomple wallets', function(done) { + var request = sinon.stub(); + + // Wallet request + request.onCall(0).yields(null, { + statusCode: 200, + }, TestData.serverResponse.incompleteWallet); + + client.request = request; + client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); + client.createAddress(function(err, x) { + err.should.contain('Incomplete'); + done(); + }); + }) + + it('should reject wallets with bad signatures', function(done) { + var request = sinon.stub(); + // Wallet request + request.onCall(0).yields(null, { + statusCode: 200, + }, TestData.serverResponse.corruptWallet22); + + client.request = request; + client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); + client.createAddress(function(err, x) { + err.should.contain('verified'); + done(); + }); + }) + it('should reject wallets with missing signatures ', function(done) { + var request = sinon.stub(); + // Wallet request + request.onCall(0).yields(null, { + statusCode: 200, + }, TestData.serverResponse.corruptWallet222); + + client.request = request; + client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); + client.createAddress(function(err, x) { + err.should.contain('verified'); + done(); + }); + }) + + it('should reject wallets missing caller"s pubkey', function(done) { + var request = sinon.stub(); + // Wallet request + request.onCall(0).yields(null, { + statusCode: 200, + }, TestData.serverResponse.missingMyPubKey); + + client.request = request; + client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); + client.createAddress(function(err, x) { + err.should.contain('verified'); + done(); + }); + }) + + + }); + + describe(' createAddress ', function() { + it(' should check address ', function(done) { + + var response = { + createdOn: 1424105995, + address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', + path: 'm/2147483647/0/7', + publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] + }; + var request = sinon.mock().yields(null, { + statusCode: 200 + }, response); + client.request = request; + + + client.createAddress(function(err, x) { + should.not.exist(err); + x.address.should.equal('2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq'); + done(); + }); + }) + it(' should detect fake addresses ', function(done) { + var response = { + createdOn: 1424105995, + address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', + path: 'm/2147483647/0/8', + publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] + }; + var request = sinon.mock().yields(null, { + statusCode: 200 + }, response); + client.request = request; + client.createAddress(function(err, x) { + err.code.should.equal('SERVERCOMPROMISED'); + err.message.should.contain('fake address'); + done(); + }); + }) + }) +}); diff --git a/test/integration/clienttestdata.js b/test/integration/clienttestdata.js new file mode 100644 index 0000000..1b7f018 --- /dev/null +++ b/test/integration/clienttestdata.js @@ -0,0 +1,116 @@ +var storage = { + wallet11: { + "m": 1, + "n": 1, + "walletPrivKey": "{\"bn\":\"6b862ffbfc90a37a2fedbbcfea91c6a4e49f49b6aaa322b6e16c46bfdbe71a38\",\"compressed\":true,\"network\":\"livenet\"}", + "network": "testnet", + "xPrivKey": "tprv8ZgxMBicQKsPeisyNJteQXZnb7CnhYc4TVAyxxicXuxMjK1rmaqVq1xnXtbSTPxUKKL9h5xJhUvw1AKfDD3i98A82eJWSYRWYjmPksewFKR", + "copayerId": "a84daa08-17b5-45ad-84cd-e275f3b07123", + "signingPrivKey": "42798f82c4ed9ace4d66335165071edf180e70bc0fc08dacb3e35185a2141d5b", + "publicKeyRing": ["tpubD6NzVbkrYhZ4YBumFxZEowDuA8iirsny2nmmFUkuxBkkZoGdPyf61Waei3tDYvVa1yqW82Xhmmd6oiibeDyM1MS3zTiky7Yg75UEV9oQhFJ"] + }, + + incompleteWallet22: { + "m": 2, + "n": 2, + "walletPrivKey":"L2Fu6TM1AqSNBaQcjgjvYjGf3EzS3MVSTwEeTw3bvy52x7ZkffWj", + "network": "testnet", + "secret": "b6f57154-0df8-4845-a61d-47ecd648c2d4:eab5a55d9214845ee8d13ea1033e42ec8d7f780ae6e521d830252a80433e91a5:T", + "xPrivKey": "tprv8ZgxMBicQKsPfFVXegcKyJjy2Y5DSrHNrtGBHG1f9pPX75QQdHwHGjWUtR7cCUXV7QcCCDon4cieHWTYscy8M7oXwF3qd3ssfBiV9M68bPB", + "copayerId": "3fc03e7a-6ebc-409b-a4b7-45b14d5a8199", + "signingPrivKey": "0d3c796fb12e387c4b5a5c566312b2b22fa0553ca041d859e3f0987215ca3a4f", + "publicKeyRing": [] + } +}; + +var serverResponse = { + completeWallet: { + wallet: { + m: 2, + n: 2, + status: 'complete', + publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' + ], + addressIndex: 0, + copayers: [{ + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', + }, { + xPubKey: 'tpubD6NzVbkrYhZ4YiXKYLGvNiQ5bZb9cBUHSBrxZn3xa6BuwZfBFgksTE8M4ZFBLWVJ4PLnAJs2JKhkpJVqsrJEAkGpb62rx62Bk4o4N5Lz8dQ', + xPubKeySignature: '3045022100e03b069db333428153c306c9bf66ebc7f25e7d7f3d087e1ca7234fbbb1a47efa02207421fb375d0dd7a7f2116301f2cdf1bce88554a6c88a82d4ec9fb37fb6680ae8', + }], + pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', + network: 'testnet', + }}, + + missingMyPubKey: { + wallet: { + m: 2, + n: 2, + status: 'complete', + publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' + ], + addressIndex: 0, + copayers: [{ + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', + }, { + xPubKey: 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV', + xPubKeySignature: '3044022025c93b418ebdbb66a0f2b21af709420e8ae769bf054f29aaa252cb5417c46a2302205e0c8b931324736b7eea4971a48039614e19abe26e13ab0ef1547aef92b55aab', + }], + pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', + network: 'testnet', + }}, + + + incompleteWallet: { + wallet: { + m: 2, + n: 2, + status: 'pending', + publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' + ], + addressIndex: 0, + copayers: [{ + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', + }], + pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', + network: 'testnet', + }}, + + corruptWallet22: { + wallet: { + m: 2, + n: 2, + status: 'complete', + publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' + ], + copayers: [{ + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', + }, { + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + xPubKeySignature: 'bababa', + }], + }}, + corruptWallet222: { + wallet: { + m: 2, + n: 2, + status: 'complete', + publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' + ], + copayers: [{ + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + }, ], + }}, +}; + +module.exports.serverResponse = serverResponse; +module.exports.storage = storage; diff --git a/test/integration.js b/test/integration/server.js similarity index 99% rename from test/integration.js rename to test/integration/server.js index c525c2b..85b6438 100644 --- a/test/integration.js +++ b/test/integration/server.js @@ -10,15 +10,15 @@ var levelup = require('levelup'); var memdown = require('memdown'); var Bitcore = require('bitcore'); -var Utils = require('../lib/utils'); -var SignUtils = require('../lib/signutils'); -var Storage = require('../lib/storage'); +var Utils = require('../../lib/utils'); +var SignUtils = require('../../lib/signutils'); +var Storage = require('../../lib/storage'); -var Wallet = require('../lib/model/wallet'); -var Address = require('../lib/model/address'); -var Copayer = require('../lib/model/copayer'); -var CopayServer = require('../lib/server'); -var TestData = require('./testdata'); +var Wallet = require('../../lib/model/wallet'); +var Address = require('../../lib/model/address'); +var Copayer = require('../../lib/model/copayer'); +var CopayServer = require('../../lib/server'); +var TestData = require('../testdata'); var helpers = {}; diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..4a52320 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--recursive