From 9a5655cb9c04997765b8816ec2097cd5bada25fa Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 11:17:44 -0300 Subject: [PATCH 01/14] rm isTEstnet to network --- bit-wallet/bit-status | 2 +- lib/client/API.js | 16 +++++++++++++++- lib/client/index.js | 1 + lib/model/wallet.js | 29 +++++------------------------ lib/server.js | 3 +-- 5 files changed, 23 insertions(+), 28 deletions(-) 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/client/API.js b/lib/client/API.js index f582925..67cb1d5 100644 --- a/lib/client/API.js +++ b/lib/client/API.js @@ -9,6 +9,7 @@ log.debug = log.verbose; var Bitcore = require('bitcore') var SignUtils = require('../signutils'); +var BitcoinUtils = require('../bitcoinutils'); var BASE_URL = 'http://localhost:3001/copay/api'; @@ -132,6 +133,7 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb m: m, n: n, walletPrivKey: privKey.toString(), + network: network, }; var args = { @@ -281,7 +283,19 @@ API.prototype.createAddress = function(cb) { var data = this._loadAndCheck(); var url = '/v1/addresses/'; - this._doPostRequest(url, {}, data, cb); + this._doPostRequest(url, {}, data, function(err, res) { + if (err) return cb(err); + + if (data.publicKeyRing.length != data.n) + return cb('Wallet Incomplete, cannot derive address.') + + var address = BitcoinUtils.deriveAddress(data.publicKeyRing, res.path, data.m, data.network); + if (address != address.address) + return cb('Server sent a fake address.'); + + + return cb(null, address); + }); }; API.prototype.history = function(limit, cb) { diff --git a/lib/client/index.js b/lib/client/index.js index 24eb668..a033b7d 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -2,6 +2,7 @@ var client = module.exports = require('./API'); client.FileStorage = require('./FileStorage'); +client.Verifier = require('./Verifier'); // TODO 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); }); }); From 1507df1bac8acfaaeec7e33cfce24b69b5a57c4f Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 11:20:19 -0300 Subject: [PATCH 02/14] add address verification --- lib/client/API.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/client/API.js b/lib/client/API.js index 67cb1d5..8667d95 100644 --- a/lib/client/API.js +++ b/lib/client/API.js @@ -283,14 +283,14 @@ API.prototype.createAddress = function(cb) { var data = this._loadAndCheck(); var url = '/v1/addresses/'; - this._doPostRequest(url, {}, data, function(err, res) { + this._doPostRequest(url, {}, data, function(err, address) { if (err) return cb(err); if (data.publicKeyRing.length != data.n) return cb('Wallet Incomplete, cannot derive address.') - var address = BitcoinUtils.deriveAddress(data.publicKeyRing, res.path, data.m, data.network); - if (address != address.address) + var local = BitcoinUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); + if (local.address != address.address || JSON.stringify(local.publicKeys)!= JSON.stringify(address.publicKeys)) return cb('Server sent a fake address.'); From 4fd81f48a8c1fa6214f0404cbf2fddac791b32d7 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 11:21:43 -0300 Subject: [PATCH 03/14] rm verfier --- bit-wallet/pepe | 1 + 1 file changed, 1 insertion(+) create mode 100644 bit-wallet/pepe diff --git a/bit-wallet/pepe b/bit-wallet/pepe new file mode 100644 index 0000000..c5f52fa --- /dev/null +++ b/bit-wallet/pepe @@ -0,0 +1 @@ +{"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"]} \ No newline at end of file From cf1884a4444277afeb46a1b998bf8a00eac99263 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 12:36:45 -0300 Subject: [PATCH 04/14] rm pepe --- bit-wallet/pepe | 1 - 1 file changed, 1 deletion(-) delete mode 100644 bit-wallet/pepe diff --git a/bit-wallet/pepe b/bit-wallet/pepe deleted file mode 100644 index c5f52fa..0000000 --- a/bit-wallet/pepe +++ /dev/null @@ -1 +0,0 @@ -{"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"]} \ No newline at end of file From c92fbb2898a7519cb3fd01ed0b76efc69aec200c Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 13:10:48 -0300 Subject: [PATCH 05/14] add verifier --- lib/client/Verifier.js | 15 +++++++++++++++ lib/client/{API.js => api.js} | 7 ++----- lib/client/{FileStorage.js => filestorage.js} | 0 lib/client/index.js | 10 +++------- 4 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 lib/client/Verifier.js rename lib/client/{API.js => api.js} (96%) rename lib/client/{FileStorage.js => filestorage.js} (100%) diff --git a/lib/client/Verifier.js b/lib/client/Verifier.js new file mode 100644 index 0000000..1fbbb47 --- /dev/null +++ b/lib/client/Verifier.js @@ -0,0 +1,15 @@ +var $ = require('preconditions').singleton(); +var _ = require('lodash'); + +var Bitcore = require('bitcore'); +var BitcoinUtils = require('../bitcoinutils') + +function Verifier(opts) {}; + +Verifier.checkAddress = function(data, address) { + var local = BitcoinUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); + if (local.address != address.address || JSON.stringify(local.publicKeys)!= JSON.stringify(address.publicKeys)) + return cb('Server sent a fake address.'); +}; + +module.exports = Verifier; diff --git a/lib/client/API.js b/lib/client/api.js similarity index 96% rename from lib/client/API.js rename to lib/client/api.js index 8667d95..9ffd70a 100644 --- a/lib/client/API.js +++ b/lib/client/api.js @@ -9,7 +9,7 @@ log.debug = log.verbose; var Bitcore = require('bitcore') var SignUtils = require('../signutils'); -var BitcoinUtils = require('../bitcoinutils'); +var Verifier = require('./verifier'); var BASE_URL = 'http://localhost:3001/copay/api'; @@ -289,10 +289,7 @@ API.prototype.createAddress = function(cb) { if (data.publicKeyRing.length != data.n) return cb('Wallet Incomplete, cannot derive address.') - var local = BitcoinUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); - if (local.address != address.address || JSON.stringify(local.publicKeys)!= JSON.stringify(address.publicKeys)) - return cb('Server sent a fake address.'); - + Verifier.checkAddress(data, address); return cb(null, address); }); diff --git a/lib/client/FileStorage.js b/lib/client/filestorage.js similarity index 100% rename from lib/client/FileStorage.js rename to lib/client/filestorage.js diff --git a/lib/client/index.js b/lib/client/index.js index a033b7d..fd416f1 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1,9 +1,5 @@ //var client = ; -var client = module.exports = require('./API'); -client.FileStorage = require('./FileStorage'); -client.Verifier = require('./Verifier'); - - -// TODO -//module.exports.storage = require('./storage'); +var client = module.exports = require('./api'); +client.FileStorage = require('./filestorage'); +client.Verifier = require('./verifier'); From 85b1bc08b22bc5ad67605f21dcd17c1291461813 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 15:02:20 -0300 Subject: [PATCH 06/14] adds tests to clientAPI and verified --- lib/client/Verifier.js | 5 ++--- lib/client/api.js | 9 +++++++-- lib/client/filestorage.js | 7 ++++--- test/{integration.js => integration/server.js} | 16 ++++++++-------- 4 files changed, 21 insertions(+), 16 deletions(-) rename test/{integration.js => integration/server.js} (99%) diff --git a/lib/client/Verifier.js b/lib/client/Verifier.js index 1fbbb47..fdb7f2c 100644 --- a/lib/client/Verifier.js +++ b/lib/client/Verifier.js @@ -1,15 +1,14 @@ var $ = require('preconditions').singleton(); var _ = require('lodash'); -var Bitcore = require('bitcore'); var BitcoinUtils = require('../bitcoinutils') function Verifier(opts) {}; Verifier.checkAddress = function(data, address) { var local = BitcoinUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); - if (local.address != address.address || JSON.stringify(local.publicKeys)!= JSON.stringify(address.publicKeys)) - return cb('Server sent a fake address.'); + return (local.address == address.address + && JSON.stringify(local.publicKeys) == JSON.stringify(address.publicKeys)); }; module.exports = Verifier; diff --git a/lib/client/api.js b/lib/client/api.js index 9ffd70a..b13c361 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -10,6 +10,7 @@ log.debug = log.verbose; var Bitcore = require('bitcore') var SignUtils = require('../signutils'); var Verifier = require('./verifier'); +var ServerCompromissedError = require('./servercompromissederror') var BASE_URL = 'http://localhost:3001/copay/api'; @@ -53,6 +54,7 @@ function API(opts) { } this.storage = opts.storage; this.verbose = !!opts.verbose; + this.request = request || opts.request; if (this.verbose) { log.level = 'debug'; } @@ -93,7 +95,7 @@ API.prototype._doRequest = function(method, url, args, data, cb) { json: true, }; log.verbose('Request Args', util.inspect(args)); - request(args, function(err, res, body) { + this.request(args, function(err, res, body) { log.verbose('Response:', err, body); if (err) return cb(err); @@ -101,6 +103,7 @@ API.prototype._doRequest = function(method, url, args, data, cb) { _parseError(body); return cb('Request error'); } + return cb(null, body); }); }; @@ -289,7 +292,9 @@ API.prototype.createAddress = function(cb) { if (data.publicKeyRing.length != data.n) return cb('Wallet Incomplete, cannot derive address.') - Verifier.checkAddress(data, address); + if (!Verifier.checkAddress(data, address)) { + return cb(new ServerCompromissedError('Server sent fake address')); + } return cb(null, address); }); diff --git a/lib/client/filestorage.js b/lib/client/filestorage.js index 27fe3d4..257cf76 100644 --- a/lib/client/filestorage.js +++ b/lib/client/filestorage.js @@ -6,17 +6,18 @@ function FileStorage(opts) { throw new Error('Please set the config filename'); } this.filename = opts.filename; + this.fs = opts.fs || fs; }; FileStorage.prototype.save = function(data) { - fs.writeFileSync(this.filename, JSON.stringify(data)); + this.fs.writeFileSync(this.filename, JSON.stringify(data)); }; FileStorage.prototype.load = function() { try { - return JSON.parse(fs.readFileSync(this.filename)); - } catch (ex) {} + return JSON.parse(this.fs.readFileSync(this.filename)); + } catch (ex) {}; }; 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 = {}; From f8b86842a619ae4451920304bedb17699fc3b6ba Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 15:04:25 -0300 Subject: [PATCH 07/14] add missing files --- lib/client/servercompromissederror.js | 6 ++ test/integration/clientApi.js | 82 +++++++++++++++++++++++++++ test/mocha.opts | 1 + 3 files changed, 89 insertions(+) create mode 100644 lib/client/servercompromissederror.js create mode 100644 test/integration/clientApi.js create mode 100644 test/mocha.opts diff --git a/lib/client/servercompromissederror.js b/lib/client/servercompromissederror.js new file mode 100644 index 0000000..d11ec91 --- /dev/null +++ b/lib/client/servercompromissederror.js @@ -0,0 +1,6 @@ +function ServerCompromissedError(message) { + this.code = 'SERVERCOMPROMISSED'; + this.message = message; +}; + +module.exports = ServerCompromissedError; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js new file mode 100644 index 0000000..4918f56 --- /dev/null +++ b/test/integration/clientApi.js @@ -0,0 +1,82 @@ +'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 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"] +}; + + + +describe('client API', function() { + + var client; + + beforeEach(function() { + + var fsmock = {};; + fsmock.readFileSync = sinon.mock().returns(JSON.stringify(wallet11)); + fsmock.writeFileSync = sinon.mock(); + var storage = new Client.FileStorage({ + filename: 'dummy', + fs: fsmock, + }); + client = new Client({ + storage: storage, + }); + }); + + 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('SERVERCOMPROMISSED'); + err.message.should.contain('fake address'); + done(); + }); + }) + +}); 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 From cdb8ab94da639196a076cbfafa6c4722ea7a71cc Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 16:23:42 -0300 Subject: [PATCH 08/14] fix spelling --- lib/client/api.js | 4 ++-- lib/client/servercompromisederror.js | 6 ++++++ lib/client/servercompromissederror.js | 6 ------ test/integration/clientApi.js | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 lib/client/servercompromisederror.js delete mode 100644 lib/client/servercompromissederror.js diff --git a/lib/client/api.js b/lib/client/api.js index b13c361..9fa5bf8 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -10,7 +10,7 @@ log.debug = log.verbose; var Bitcore = require('bitcore') var SignUtils = require('../signutils'); var Verifier = require('./verifier'); -var ServerCompromissedError = require('./servercompromissederror') +var ServerCompromisedError = require('./servercompromisederror') var BASE_URL = 'http://localhost:3001/copay/api'; @@ -293,7 +293,7 @@ API.prototype.createAddress = function(cb) { return cb('Wallet Incomplete, cannot derive address.') if (!Verifier.checkAddress(data, address)) { - return cb(new ServerCompromissedError('Server sent fake address')); + return cb(new ServerCompromisedError('Server sent fake address')); } return cb(null, address); 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/client/servercompromissederror.js b/lib/client/servercompromissederror.js deleted file mode 100644 index d11ec91..0000000 --- a/lib/client/servercompromissederror.js +++ /dev/null @@ -1,6 +0,0 @@ -function ServerCompromissedError(message) { - this.code = 'SERVERCOMPROMISSED'; - this.message = message; -}; - -module.exports = ServerCompromissedError; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 4918f56..d39bada 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -73,7 +73,7 @@ describe('client API', function() { }, response); client.request = request; client.createAddress(function(err, x) { - err.code.should.equal('SERVERCOMPROMISSED'); + err.code.should.equal('SERVERCOMPROMISED'); err.message.should.contain('fake address'); done(); }); From 57532242a468bf76a6b2820750c231a695084a0f Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 16:57:40 -0300 Subject: [PATCH 09/14] add preconditons --- bit-wallet/bit-reject | 0 bit-wallet/bit-rm | 0 lib/client/api.js | 4 +--- 3 files changed, 1 insertion(+), 3 deletions(-) mode change 100644 => 100755 bit-wallet/bit-reject mode change 100644 => 100755 bit-wallet/bit-rm 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/lib/client/api.js b/lib/client/api.js index 9fa5bf8..39081ef 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -284,14 +284,12 @@ API.prototype.createAddress = function(cb) { var self = this; var data = this._loadAndCheck(); + $.checkState(data.publicKeyRing.length != data.n, 'Wallet Incomplete, cannot derive address.'); var url = '/v1/addresses/'; this._doPostRequest(url, {}, data, function(err, address) { if (err) return cb(err); - if (data.publicKeyRing.length != data.n) - return cb('Wallet Incomplete, cannot derive address.') - if (!Verifier.checkAddress(data, address)) { return cb(new ServerCompromisedError('Server sent fake address')); } From 673ba2823b58d747cb902545866e9573ab8da369 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 19:54:38 -0300 Subject: [PATCH 10/14] working after refactor --- bit-wallet/bit-join | 2 +- lib/bitcoinutils.js | 25 +++++++++++++++++++++++++ lib/client/Verifier.js | 5 ++--- lib/client/api.js | 21 ++++++++++++--------- test/integration/clientApi.js | 3 +-- 5 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 lib/bitcoinutils.js 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/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/Verifier.js b/lib/client/Verifier.js index fdb7f2c..580468b 100644 --- a/lib/client/Verifier.js +++ b/lib/client/Verifier.js @@ -6,9 +6,8 @@ var BitcoinUtils = require('../bitcoinutils') 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)); + 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)); }; module.exports = Verifier; diff --git a/lib/client/api.js b/lib/client/api.js index 39081ef..1bfdd22 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -1,6 +1,7 @@ 'use strict'; var _ = require('lodash'); +var $ = require('preconditions').singleton(); var util = require('util'); var async = require('async'); var log = require('npmlog'); @@ -61,7 +62,7 @@ function API(opts) { }; -API.prototype._loadAndCheck = function() { +API.prototype._loadAndCheck = function(opts) { var data = this.storage.load(); if (!data) { log.error('Wallet file not found.'); @@ -69,11 +70,15 @@ API.prototype._loadAndCheck = function() { } 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); + throw new Error('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); } if (data.n > 1) { var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; + + if (opts.requireCompletePKR && !pkrComplete) { + throw new Error('Wallet Incomplete, cannot derive address.'); + } + 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.') } @@ -135,7 +140,7 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb data = { m: m, n: n, - walletPrivKey: privKey.toString(), + walletPrivKey: privKey.toWIF(), network: network, }; @@ -193,11 +198,12 @@ API.prototype._joinWallet = function(data, secret, copayerName, cb) { this._doPostRequest(url, args, data, function(err, body) { var wallet = body.wallet; data.copayerId = body.copayerId; - data.walletPrivKey = walletPrivKey; + 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); return cb(); @@ -283,13 +289,10 @@ API.prototype.getAddresses = function(cb) { API.prototype.createAddress = function(cb) { var self = this; - var data = this._loadAndCheck(); - $.checkState(data.publicKeyRing.length != data.n, 'Wallet Incomplete, cannot derive address.'); - + var data = this._loadAndCheck({requireCompletePKR: true}); var url = '/v1/addresses/'; this._doPostRequest(url, {}, data, function(err, address) { if (err) return cb(err); - if (!Verifier.checkAddress(data, address)) { return cb(new ServerCompromisedError('Server sent fake address')); } diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index d39bada..ba8d8f6 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -60,7 +60,6 @@ describe('client API', function() { done(); }); }) - }); it('should detect fake addresses', function(done) { var response = { createdOn: 1424105995, @@ -78,5 +77,5 @@ describe('client API', function() { done(); }); }) - + }); }); From 2121565070cccc65baa4a868090ab1faec0539e2 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 20:23:25 -0300 Subject: [PATCH 11/14] async storage --- lib/client/api.js | 338 ++++++++++++++++++---------------- lib/client/filestorage.js | 17 +- test/integration/clientApi.js | 30 ++- 3 files changed, 219 insertions(+), 166 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 1bfdd22..f256b1c 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -62,28 +62,24 @@ function API(opts) { }; -API.prototype._loadAndCheck = function(opts) { - var data = this.storage.load(); - if (!data) { - log.error('Wallet file not found.'); - process.exit(1); - } - - if (data.verified == 'corrupt') { - throw new Error('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); - } - if (data.n > 1) { - var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; - - if (opts.requireCompletePKR && !pkrComplete) { - throw new Error('Wallet Incomplete, cannot derive address.'); +API.prototype._loadAndCheck = function(opts, cb) { + this.storage.load(function(err, data) { + if (err || !data) { + return cb(err || 'Wallet file not found.'); } - 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.') + if (data.verified == 'corrupt') { + return cb('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); } - } - return data; + if (data.n > 1) { + var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; + + if (opts.requireCompletePKR && !pkrComplete) { + return cb('Wallet Incomplete, cannot derive address'); + } + } + return cb(null, data); + }); }; API.prototype._doRequest = function(method, url, args, data, cb) { @@ -130,44 +126,48 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb 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'); + this.storage.load(function(err, data) { + if (data) + return cb('Storage already contains a wallet'); - // Generate wallet key pair to verify copayers - var privKey = new Bitcore.PrivateKey(null, network); - var pubKey = privKey.toPublicKey(); + 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, - }; + 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/'; + 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) { + self._doPostRequest(url, args, data, function(err, body) { if (err) return cb(err); - return cb(null, ret); + 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); + }); + + }); }); }); }; @@ -204,54 +204,58 @@ API.prototype._joinWallet = function(data, secret, copayerName, cb) { data.n = wallet.n; data.publicKeyRing = wallet.publicKeyRing; data.network = wallet.network, - self.storage.save(data); - - return cb(); + self.storage.save(data, 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'); + this.storage.load(function(err, data) { + if (data) + return cb('Storage already contains a wallet'); - self._joinWallet(data, secret, copayerName, cb); + 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) { + this._loadAndCheck({}, function(err, data) { 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) { + var url = '/v1/wallets/'; + self._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.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); + 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'; } - }); - 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, function(err) { + return cb(err, wallet); + }); } - self.storage.save(data); - } - return cb(null, wallet); + return cb(null, wallet); + }); }); }; @@ -266,38 +270,35 @@ API.prototype.getStatus = function(cb) { API.prototype.sendTxProposal = function(inArgs, cb) { var self = this; - var data = this._loadAndCheck(); - var args = _createProposalOpts(inArgs, data.signingPrivKey); + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); - var url = '/v1/txproposals/'; - this._doPostRequest(url, args, data, cb); + var args = _createProposalOpts(inArgs, data.signingPrivKey); + + var url = '/v1/txproposals/'; + self._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({requireCompletePKR: true}); - var url = '/v1/addresses/'; - this._doPostRequest(url, {}, data, function(err, address) { + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { if (err) return cb(err); - if (!Verifier.checkAddress(data, address)) { - return cb(new ServerCompromisedError('Server sent fake address')); - } - return cb(null, address); + 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); + }); }); }; @@ -308,92 +309,113 @@ 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); + 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; - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/'; - this._doGetRequest(url, data, cb); + this._loadAndCheck({ + requireCompletePKR: true + }, 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; - var data = this._loadAndCheck(); + + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); - //Derive proper key to sign, for each input - var privs = [], - derived = {}; + //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); + 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]); + _.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); }); - - 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); + this._loadAndCheck({ + requireCompletePKR: true + }, 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; - var data = this._loadAndCheck(); - var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - this._doPostRequest(url, {}, data, cb); + this._loadAndCheck({ + requireCompletePKR: true + }, 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; - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/' + txp.id; - - this._doRequest('delete', url, {}, data, cb); + this._loadAndCheck({ + requireCompletePKR: true + }, 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 index 257cf76..35e6e69 100644 --- a/lib/client/filestorage.js +++ b/lib/client/filestorage.js @@ -10,14 +10,19 @@ function FileStorage(opts) { }; -FileStorage.prototype.save = function(data) { - this.fs.writeFileSync(this.filename, JSON.stringify(data)); +FileStorage.prototype.save = function(data, cb) { + this.fs.writeFile(this.filename, JSON.stringify(data), cb); }; -FileStorage.prototype.load = function() { - try { - return JSON.parse(this.fs.readFileSync(this.filename)); - } catch (ex) {}; +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); + }); }; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index ba8d8f6..12fe14b 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -19,6 +19,17 @@ var wallet11 = { "publicKeyRing": ["tpubD6NzVbkrYhZ4YBumFxZEowDuA8iirsny2nmmFUkuxBkkZoGdPyf61Waei3tDYvVa1yqW82Xhmmd6oiibeDyM1MS3zTiky7Yg75UEV9oQhFJ"] }; +var incompleteWallet22 = { + "m": 2, + "n": 2, + "walletPrivKey": "L3XSE3KNjQM1XRP1h5yMCSKsN4hs3D6eK7Vwn5M88Bs6jpCnXR3R", + "network": "testnet", + "secret": "d9cf45a1-6793-4df4-94df-c99d2c2e1fe9:bc2488c1b83e455a4b908a0d0aeaf70351efc48fbcaa454bffefdef419a5ee6a:T", + "xPrivKey": "tprv8ZgxMBicQKsPdoC5DGtnXx7fp7YnUtGv8b7fU2oDQfDpHFQh1QCgpKc8GHpdsBN5THaHYMV5LgD5cP5NYaacGVr786p3mVLSZff9berTV8h", + "copayerId": "c3a33ca0-37cf-4e80-b745-71272683835c", + "signingPrivKey": "6e129c4996666e5ecdf78aed626c01977fa19eacce6659738ebe065f86523e9b", + "publicKeyRing": [] +}; describe('client API', function() { @@ -28,8 +39,8 @@ describe('client API', function() { beforeEach(function() { var fsmock = {};; - fsmock.readFileSync = sinon.mock().returns(JSON.stringify(wallet11)); - fsmock.writeFileSync = sinon.mock(); + fsmock.readFile = sinon.mock().yields(null, JSON.stringify(wallet11)); + fsmock.writeFile = sinon.mock().yields(); var storage = new Client.FileStorage({ filename: 'dummy', fs: fsmock, @@ -77,5 +88,20 @@ describe('client API', function() { done(); }); }) + + it('should complain wallet is not complete', function(done) { + var request = sinon.mock().yields(null, { + statusCode: 200 + }, { + dummy: true + }); + client.request = request; + client.storage.fs.readFile = sinon.mock().yields(null, JSON.stringify(incompleteWallet22)); + client.createAddress(function(err, x) { + err.should.contain('Incomplete'); + done(); + }); + }) + }); }); From d56dcd41de4cabc0724616003f39d53562d45973 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 21:10:14 -0300 Subject: [PATCH 12/14] completes pkr --- lib/client/api.js | 244 +++++++++++++++++++++++----------------------- 1 file changed, 123 insertions(+), 121 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index f256b1c..4aa554a 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -62,7 +62,43 @@ function API(opts) { }; -API.prototype._loadAndCheck = function(opts, cb) { +API.prototype._tryToComplete = function(data, cb) { + var self = this; + var isCorrupted; + + var url = '/v1/wallets/'; + self._doGetRequest(url, data, function(err, wallet) { + if (err) return cb(err); + 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) { + if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { + fake.push(copayer); + } + }); + if (fake.length > 0) { + isCorrupted = true; + data.verified = 'corrupt'; + } else { + data.verified = 'ok'; + } + self.storage.save(data, function(err) { + if (isCorrupted) { + return cb('Some copayers in the wallet could not be verified to have known the wallet secret'); + } + return cb(err, data); + }); + } + return cb(null, 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.'); @@ -74,8 +110,8 @@ API.prototype._loadAndCheck = function(opts, cb) { if (data.n > 1) { var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; - if (opts.requireCompletePKR && !pkrComplete) { - return cb('Wallet Incomplete, cannot derive address'); + if (!pkrComplete) { + return self._tryToComplete(data, cb); } } return cb(null, data); @@ -222,39 +258,12 @@ API.prototype.joinWallet = function(secret, copayerName, cb) { API.prototype.getStatus = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck(function(err, data) { if (err) return cb(err); var url = '/v1/wallets/'; self._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, function(err) { - return cb(err, wallet); - }); - } - - return cb(null, wallet); + return cb(err, body); }); }); }; @@ -270,36 +279,34 @@ API.prototype.getStatus = function(cb) { API.prototype.sendTxProposal = function(inArgs, cb) { var self = this; - this._loadAndCheck({ - requireCompletePKR: true - }, function(err, data) { - if (err) return cb(err); + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); - var args = _createProposalOpts(inArgs, data.signingPrivKey); + var args = _createProposalOpts(inArgs, data.signingPrivKey); - var url = '/v1/txproposals/'; - self._doPostRequest(url, args, data, cb); - }); + var url = '/v1/txproposals/'; + self._doPostRequest(url, args, data, cb); + }); }; API.prototype.createAddress = function(cb) { var self = this; - this._loadAndCheck({ - requireCompletePKR: true - }, function(err, data) { - if (err) return cb(err); - - var url = '/v1/addresses/'; - self._doPostRequest(url, {}, data, function(err, address) { + this._loadAndCheck( + function(err, data) { if (err) return cb(err); - if (!Verifier.checkAddress(data, address)) { - return cb(new ServerCompromisedError('Server sent fake address')); - } - return cb(null, address); + 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) { @@ -309,7 +316,7 @@ API.prototype.history = function(limit, cb) { API.prototype.getBalance = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck(function(err, data) { if (err) return cb(err); var url = '/v1/balance/'; self._doGetRequest(url, data, cb); @@ -320,102 +327,97 @@ API.prototype.getBalance = function(cb) { API.prototype.getTxProposals = function(opts, cb) { var self = this; - this._loadAndCheck({ - requireCompletePKR: true - }, function(err, data) { - if (err) return cb(err); - var url = '/v1/txproposals/'; - self._doGetRequest(url, data, cb); - }); + 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({ - requireCompletePKR: true - }, function(err, data) { - if (err) return cb(err); + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); - //Derive proper key to sign, for each input - var privs = [], - derived = {}; + //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); + 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]); + _.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); }); - - 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({ - requireCompletePKR: true - }, function(err, data) { - if (err) return cb(err); + 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); - }); + 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({ - requireCompletePKR: true - }, function(err, data) { - if (err) return cb(err); + this._loadAndCheck( + function(err, data) { + if (err) return cb(err); - var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - self._doPostRequest(url, {}, data, cb); - }); + var url = '/v1/txproposals/' + txp.id + '/broadcast/'; + self._doPostRequest(url, {}, data, cb); + }); }; API.prototype.removeTxProposal = function(txp, cb) { var self = this; - this._loadAndCheck({ - requireCompletePKR: true - }, function(err, data) { - if (err) return cb(err); - var url = '/v1/txproposals/' + txp.id; - self._doRequest('delete', url, {}, data, cb); - }); + 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; From 196ae2a4481b5c47965f8028f48a55a5decd3503 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 17 Feb 2015 02:20:04 -0300 Subject: [PATCH 13/14] add test --- lib/client/api.js | 87 ++++++++++++--------- test/integration/clientApi.js | 121 ++++++++++++++++++----------- test/integration/clienttestdata.js | 92 ++++++++++++++++++++++ 3 files changed, 218 insertions(+), 82 deletions(-) create mode 100644 test/integration/clienttestdata.js diff --git a/lib/client/api.js b/lib/client/api.js index 4aa554a..a15e01d 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -62,35 +62,49 @@ function API(opts) { }; +API.prototype._isWalletCorrupt = function(wallet, data) { + var pubKey = Bitcore.PrivateKey.fromString(data.walletPrivKey).toPublicKey().toString(); + var fake = []; + + if (data.n != wallet.copayers.length) + return true; + + var uniq = []; + _.each(wallet.copayers, function(copayer) { + if (uniq[copayer.xPubKey]++) + return true; + + if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { + fake.push(copayer); + } + }); + return fake.length > 0; +}; + + API.prototype._tryToComplete = function(data, cb) { var self = this; - var isCorrupted; var url = '/v1/wallets/'; self._doGetRequest(url, data, function(err, wallet) { if (err) return cb(err); - 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) { - if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { - fake.push(copayer); - } - }); - if (fake.length > 0) { - isCorrupted = true; + + if (wallet.status === 'complete' && !data.verified) { + + if (self._isWalletCorrupt(wallet, data)) { data.verified = 'corrupt'; } else { data.verified = 'ok'; } self.storage.save(data, function(err) { - if (isCorrupted) { + if (data.verified == 'corrupt') { return cb('Some copayers in the wallet could not be verified to have known the wallet secret'); } return cb(err, data); }); + } else { + return cb('Wallet Incomplete'); } - return cb(null, data); }); }; @@ -98,12 +112,12 @@ API.prototype._tryToComplete = function(data, cb) { API.prototype._loadAndCheck = function(cb) { - var self = this; + var self = this; + this.storage.load(function(err, data) { if (err || !data) { return cb(err || 'Wallet file not found.'); } - if (data.verified == 'corrupt') { return cb('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); } @@ -131,10 +145,13 @@ API.prototype._doRequest = function(method, url, args, data, cb) { body: args, json: true, }; - log.verbose('Request Args', util.inspect(args)); + log.verbose('Request Args', util.inspect(args, { + depth: 10 + })); this.request(args, function(err, res, body) { - log.verbose('Response:', err, body); - + log.verbose(util.inspect(body, { + depth: 10 + })); if (err) return cb(err); if (res.statusCode != 200) { _parseError(body); @@ -279,34 +296,32 @@ API.prototype.getStatus = function(cb) { API.prototype.sendTxProposal = function(inArgs, cb) { var self = this; - this._loadAndCheck( - function(err, data) { - if (err) return cb(err); + this._loadAndCheck(function(err, data) { + if (err) return cb(err); - var args = _createProposalOpts(inArgs, data.signingPrivKey); + var args = _createProposalOpts(inArgs, data.signingPrivKey); - var url = '/v1/txproposals/'; - self._doPostRequest(url, args, data, cb); - }); + var url = '/v1/txproposals/'; + self._doPostRequest(url, args, data, cb); + }); }; API.prototype.createAddress = function(cb) { var self = this; - this._loadAndCheck( - function(err, data) { + 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')); + } - 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); - }); + return cb(null, address); }); + }); }; API.prototype.history = function(limit, cb) { diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 12fe14b..6d52304 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -7,51 +7,95 @@ var should = chai.should(); var Client = require('../../lib/client'); var API = Client.API; var Bitcore = require('bitcore'); +var TestData = require('./clienttestdata'); -var 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"] -}; - -var incompleteWallet22 = { - "m": 2, - "n": 2, - "walletPrivKey": "L3XSE3KNjQM1XRP1h5yMCSKsN4hs3D6eK7Vwn5M88Bs6jpCnXR3R", - "network": "testnet", - "secret": "d9cf45a1-6793-4df4-94df-c99d2c2e1fe9:bc2488c1b83e455a4b908a0d0aeaf70351efc48fbcaa454bffefdef419a5ee6a:T", - "xPrivKey": "tprv8ZgxMBicQKsPdoC5DGtnXx7fp7YnUtGv8b7fU2oDQfDpHFQh1QCgpKc8GHpdsBN5THaHYMV5LgD5cP5NYaacGVr786p3mVLSZff9berTV8h", - "copayerId": "c3a33ca0-37cf-4e80-b745-71272683835c", - "signingPrivKey": "6e129c4996666e5ecdf78aed626c01977fa19eacce6659738ebe065f86523e9b", - "publicKeyRing": [] -}; - - -describe('client API', function() { +describe(' client API ', function() { var client; beforeEach(function() { var fsmock = {};; - fsmock.readFile = sinon.mock().yields(null, JSON.stringify(wallet11)); + 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, + storage: storage }); }); - describe('createAddress', function() { - it('should check address', function(done) { + 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 complain wallet is not complete ', 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(); + }); + }) + }); + + describe(' createAddress ', function() { + it(' should check address ', function(done) { var response = { createdOn: 1424105995, @@ -71,7 +115,7 @@ describe('client API', function() { done(); }); }) - it('should detect fake addresses', function(done) { + it(' should detect fake addresses ', function(done) { var response = { createdOn: 1424105995, address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', @@ -88,20 +132,5 @@ describe('client API', function() { done(); }); }) - - it('should complain wallet is not complete', function(done) { - var request = sinon.mock().yields(null, { - statusCode: 200 - }, { - dummy: true - }); - client.request = request; - client.storage.fs.readFile = sinon.mock().yields(null, JSON.stringify(incompleteWallet22)); - client.createAddress(function(err, x) { - err.should.contain('Incomplete'); - done(); - }); - }) - - }); + }) }); diff --git a/test/integration/clienttestdata.js b/test/integration/clienttestdata.js new file mode 100644 index 0000000..175658c --- /dev/null +++ b/test/integration/clienttestdata.js @@ -0,0 +1,92 @@ +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: { + 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: { + 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: { + m: 2, + n: 2, + status: 'complete', + publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' + ], + copayers: [{ + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', + }, { + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + xPubKeySignature: 'bababa', + }], + }, + corruptWallet222: { + m: 2, + n: 2, + status: 'complete', + publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' + ], + copayers: [{ + xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', + }, ], + } +}; + +module.exports.serverResponse = serverResponse; +module.exports.storage = storage; From 03adc38897f444528744678c204fd7d04521c679 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 17 Feb 2015 11:48:19 -0300 Subject: [PATCH 14/14] add tests, refactor checks --- lib/client/Verifier.js | 46 ++++++++++++++++++++++++++ lib/client/api.js | 52 +++++++----------------------- test/integration/clientApi.js | 23 +++++++++++-- test/integration/clienttestdata.js | 32 +++++++++++++++--- 4 files changed, 106 insertions(+), 47 deletions(-) diff --git a/lib/client/Verifier.js b/lib/client/Verifier.js index 580468b..42ae592 100644 --- a/lib/client/Verifier.js +++ b/lib/client/Verifier.js @@ -1,7 +1,14 @@ 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) {}; @@ -10,4 +17,43 @@ Verifier.checkAddress = function(data, address) { 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 index a15e01d..066308d 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -62,55 +62,30 @@ function API(opts) { }; -API.prototype._isWalletCorrupt = function(wallet, data) { - var pubKey = Bitcore.PrivateKey.fromString(data.walletPrivKey).toPublicKey().toString(); - var fake = []; - - if (data.n != wallet.copayers.length) - return true; - - var uniq = []; - _.each(wallet.copayers, function(copayer) { - if (uniq[copayer.xPubKey]++) - return true; - - if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { - fake.push(copayer); - } - }); - return fake.length > 0; -}; - API.prototype._tryToComplete = function(data, cb) { var self = this; var url = '/v1/wallets/'; - self._doGetRequest(url, data, function(err, wallet) { + self._doGetRequest(url, data, function(err, ret) { if (err) return cb(err); + var wallet = ret.wallet; - if (wallet.status === 'complete' && !data.verified) { - - if (self._isWalletCorrupt(wallet, data)) { - data.verified = 'corrupt'; - } else { - data.verified = 'ok'; - } - self.storage.save(data, function(err) { - if (data.verified == 'corrupt') { - return cb('Some copayers in the wallet could not be verified to have known the wallet secret'); - } - return cb(err, data); - }); - } else { + 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; @@ -118,9 +93,6 @@ API.prototype._loadAndCheck = function(cb) { if (err || !data) { return cb(err || 'Wallet file not found.'); } - if (data.verified == 'corrupt') { - return cb('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); - } if (data.n > 1) { var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 6d52304..07f29fd 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -48,7 +48,7 @@ describe(' client API ', function() { }) - it('should complain wallet is not complete ', function(done) { + it('should handle incomple wallets', function(done) { var request = sinon.stub(); // Wallet request @@ -64,7 +64,7 @@ describe(' client API ', function() { }); }) - it(' should reject wallets with bad signatures', function(done) { + it('should reject wallets with bad signatures', function(done) { var request = sinon.stub(); // Wallet request request.onCall(0).yields(null, { @@ -78,7 +78,7 @@ describe(' client API ', function() { done(); }); }) - it(' should reject wallets with missing signatures ', function(done) { + it('should reject wallets with missing signatures ', function(done) { var request = sinon.stub(); // Wallet request request.onCall(0).yields(null, { @@ -92,6 +92,23 @@ describe(' client API ', function() { 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() { diff --git a/test/integration/clienttestdata.js b/test/integration/clienttestdata.js index 175658c..1b7f018 100644 --- a/test/integration/clienttestdata.js +++ b/test/integration/clienttestdata.js @@ -25,6 +25,27 @@ var storage = { 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', @@ -41,10 +62,11 @@ var serverResponse = { }], pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', network: 'testnet', - }, + }}, incompleteWallet: { + wallet: { m: 2, n: 2, status: 'pending', @@ -58,9 +80,10 @@ var serverResponse = { }], pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', network: 'testnet', - }, + }}, corruptWallet22: { + wallet: { m: 2, n: 2, status: 'complete', @@ -74,8 +97,9 @@ var serverResponse = { xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', xPubKeySignature: 'bababa', }], - }, + }}, corruptWallet222: { + wallet: { m: 2, n: 2, status: 'complete', @@ -85,7 +109,7 @@ var serverResponse = { copayers: [{ xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', }, ], - } + }}, }; module.exports.serverResponse = serverResponse;