diff --git a/lib/errors/errordefinitions.js b/lib/errors/errordefinitions.js index 8a7bdbd..52da5e6 100644 --- a/lib/errors/errordefinitions.js +++ b/lib/errors/errordefinitions.js @@ -15,8 +15,10 @@ var errors = { INSUFFICIENT_FUNDS: 'Insufficient funds', INSUFFICIENT_FUNDS_FOR_FEE: 'Insufficient funds for fee', INVALID_ADDRESS: 'Invalid address', + KEY_IN_COPAYER: 'Key already registered', LOCKED_FUNDS: 'Funds are locked by pending transaction proposals', NOT_AUTHORIZED: 'Not authorized', + TO_MANY_KEYS: 'To many keys registered', TX_ALREADY_BROADCASTED: 'The transaction proposal is already broadcasted', TX_CANNOT_CREATE: 'Cannot create TX proposal during backoff time', TX_CANNOT_REMOVE: 'Cannot remove this tx proposal during locktime', diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index f541c79..042ee87 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -70,6 +70,8 @@ TxProposal.create = function(opts) { x.fee = null; x.feePerKb = opts.feePerKb; x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos; + x.proposalSignaturePubKey = opts.proposalSignaturePubKey; + x.proposalSignaturePubKeySig = opts.proposalSignaturePubKeySig; if (_.isFunction(TxProposal._create[x.type])) { TxProposal._create[x.type](x, opts); @@ -114,6 +116,8 @@ TxProposal.fromObj = function(obj) { x.network = obj.network; x.feePerKb = obj.feePerKb; x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; + x.proposalSignaturePubKey = obj.proposalSignaturePubKey; + x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig; return x; }; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index d366d24..e32ef95 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -107,21 +107,18 @@ Wallet.prototype.addCopayer = function(copayer) { this._updatePublicKeyRing(); }; -Wallet.prototype.addCopayerRequestKey = function(copayerId, requestPubKey, signature) { +Wallet.prototype.addCopayerRequestKey = function(copayerId, requestPubKey, signature, restrictions) { $.checkState(this.copayers.length == this.n); - var c = _.find(this.copayers, { - id: copayerId, - }); - $.checkState(c); + var c = this.getCopayer(copayerId); //new ones go first c.requestPubKeys.unshift({ - key: requestPubKey, + key: requestPubKey.toString(), signature: signature, + selfSigned: true, + restrictions: restrictions, }); - - //this._updatePublicKeyRing(); }; Wallet.prototype.getCopayer = function(copayerId) { diff --git a/lib/server.js b/lib/server.js index b5238ef..4bc5c3f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -33,7 +33,7 @@ var blockchainExplorer; var blockchainExplorerOpts; var messageBroker; - +var MAX_KEYS = 100; /** * Creates an instance of the Bitcore Wallet Service. @@ -177,7 +177,7 @@ WalletService.getInstanceWithAuth = function(opts, cb) { if (err) return cb(err); if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found')); - var isValid = server._verifySignatureAgainstArray(opts.message, opts.signature, copayer.requestPubKeys); + var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys); if (!isValid) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature')); @@ -289,9 +289,9 @@ WalletService.prototype._verifySignature = function(text, signature, pubkey) { * @param signature * @param pubKeys */ -WalletService.prototype._verifySignatureAgainstArray = function(text, signature, pubKeys) { +WalletService.prototype._getSigningKey = function(text, signature, pubKeys) { var self = this; - return _.any(pubKeys, function(item) { + return _.find(pubKeys, function(item) { return self._verifySignature(text, signature, item.key); }); }; @@ -337,6 +337,110 @@ WalletService.prototype._notify = function(type, data, opts, cb) { }; +WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) { + var self = this; + + if (wallet.copayers.length == wallet.n) return cb(Errors.WALLET_FULL); + + var copayer = Model.Copayer.create({ + name: opts.name, + copayerIndex: wallet.copayers.length, + xPubKey: opts.xPubKey, + requestPubKey: opts.requestPubKey, + signature: opts.copayerSignature, + }); + + self.storage.fetchCopayerLookup(copayer.id, function(err, res) { + if (err) return cb(err); + if (res) return cb(Errors.COPAYER_REGISTERED); + + wallet.addCopayer(copayer); + self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { + if (err) return cb(err); + + async.series([ + + function(next) { + self._notify('NewCopayer', { + walletId: opts.walletId, + copayerId: copayer.id, + copayerName: copayer.name, + }, next); + }, + function(next) { + if (wallet.isComplete() && wallet.isShared()) { + self._notify('WalletComplete', { + walletId: opts.walletId, + }, { + isGlobal: true + }, next); + } else { + next(); + } + }, + ], function() { + return cb(null, { + copayerId: copayer.id, + wallet: wallet + }); + }); + }); + }); +}; + + +WalletService.prototype._addKeyToCopayer = function(wallet, copayer, opts, cb) { + var self = this; + wallet.addCopayerRequestKey(copayer.copayerId, opts.requestPubKey, opts.signature, opts.restrictions); + self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { + if (err) return cb(err); + + return cb(null, { + copayerId: copayer.id, + wallet: wallet + }); + }); +}; + +/** + * Adds access to a given copayer + * + * @param {Object} opts + * @param {string} opts.copayerId - The copayer id + * @param {string} opts.requestPubKey - Public Key used to check requests from this copayer. + * @param {string} opts.copayerSignature - S(requestPubKey). Used by other copayers to verify the that the copayer is himself (signed with REQUEST_KEY_AUTH) + * @param {string} opts.restrictions + * - cannotProposeTXs + * - cannotXXX TODO + */ +WalletService.prototype.addAccess = function(opts, cb) { + var self = this; + + if (!Utils.checkRequired(opts, ['copayerId', 'requestPubKey', 'signature'])) + return cb(new ClientError('Required argument missing')); + + self.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) { + if (err) return cb(err); + if (!copayer) return cb(Errors.NOT_AUTHORIZED); + self.storage.fetchWallet(copayer.walletId, function(err, wallet) { + if (err) return cb(err); + if (!wallet) return cb(Errors.NOT_AUTHORIZED); + + var xPubKey = _.find(wallet.copayers, { + id: opts.copayerId + }).xPubKey; + if (!WalletUtils.checkRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) { + return cb(Errors.NOT_AUTHORIZED); + } + + if (copayer.requestPubKeys.length > MAX_KEYS) + return cb(Errors.TO_MANY_KEYS); + + self._addKeyToCopayer(wallet, copayer, opts, cb); + }); + }); +}; + /** * Joins a wallet in creation. * @param {Object} opts @@ -371,52 +475,7 @@ WalletService.prototype.joinWallet = function(opts, cb) { xPubKey: opts.xPubKey })) return cb(Errors.COPAYER_IN_WALLET); - if (wallet.copayers.length == wallet.n) return cb(Errors.WALLET_FULL); - - var copayer = Model.Copayer.create({ - name: opts.name, - copayerIndex: wallet.copayers.length, - xPubKey: opts.xPubKey, - requestPubKey: opts.requestPubKey, - signature: opts.copayerSignature, - }); - - self.storage.fetchCopayerLookup(copayer.id, function(err, res) { - if (err) return cb(err); - if (res) return cb(Errors.COPAYER_REGISTERED); - - wallet.addCopayer(copayer); - self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { - if (err) return cb(err); - - async.series([ - - function(next) { - self._notify('NewCopayer', { - walletId: opts.walletId, - copayerId: copayer.id, - copayerName: copayer.name, - }, next); - }, - function(next) { - if (wallet.isComplete() && wallet.isShared()) { - self._notify('WalletComplete', { - walletId: opts.walletId, - }, { - isGlobal: true - }, next); - } else { - next(); - } - }, - ], function() { - return cb(null, { - copayerId: copayer.id, - wallet: wallet - }); - }); - }); - }); + self._addCopayerToWallet(wallet, opts, cb); }); }); }; @@ -559,7 +618,7 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); - var isValid = self._verifySignatureAgainstArray(opts.message, opts.signature, copayer.requestPubKeys); + var isValid = !!self._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys); return cb(null, isValid); }); }; @@ -707,7 +766,6 @@ WalletService.prototype.getBalance = function(opts, cb) { self.getUtxos({}, function(err, utxos) { if (err) return cb(err); - var balance = self._totalizeUtxos(utxos); // Compute balance by address @@ -980,7 +1038,8 @@ WalletService.prototype.createTx = function(opts, cb) { hash = WalletUtils.getProposalHash(header) } - if (!self._verifySignatureAgainstArray(hash, opts.proposalSignature, copayer.requestPubKeys)) + var signingKey = self._getSigningKey(hash, opts.proposalSignature, copayer.requestPubKeys) + if (!signingKey) return cb(new ClientError('Invalid proposal signature')); self._canCreateTx(self.copayerId, function(err, canCreate) { @@ -1014,7 +1073,7 @@ WalletService.prototype.createTx = function(opts, cb) { valid: false })) return; - var txp = Model.TxProposal.create({ + var txOpts = { type: type, walletId: self.walletId, creatorId: self.copayerId, @@ -1030,7 +1089,14 @@ WalletService.prototype.createTx = function(opts, cb) { requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), walletN: wallet.n, excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, - }); + }; + + if (signingKey.selfSigned) { + txOpts.proposalSignaturePubKey = signingKey.key; + txOpts.proposalSignaturePubKeySig = signingKey.signature; + } + + var txp = Model.TxProposal.create(txOpts); if (!self.clientVersion || /^bw.-0\.0\./.test(self.clientVersion)) { txp.version = '1.0.1'; diff --git a/test/integration/server.js b/test/integration/server.js index bf4b919..c8a227e 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -26,6 +26,7 @@ var WalletService = require('../../lib/server'); var EmailService = require('../../lib/emailservice'); var TestData = require('../testdata'); +var CLIENT_VERSION = 'bwc-0.1.1'; var helpers = {}; helpers.getAuthServer = function(copayerId, cb) { @@ -1405,31 +1406,154 @@ describe('Wallet service', function() { }); - describe.skip('Multiple request Pub Keys', function() { + describe('Multiple request Pub Keys', function() { var server, wallet; - beforeEach(function(done) { - helpers.createAndJoinWallet(2, 2, function(s, w) { - server = s; - wallet = w; - done(); + var opts, reqPrivKey, ws; + var getAuthServer = function(copayerId, privKey, cb) { + var msg = 'dummy'; + var sig = WalletUtils.signMessage(msg, privKey); + WalletService.getInstanceWithAuth({ + copayerId: copayerId, + message: msg, + signature: sig, + clientVersion: CLIENT_VERSION, + }, function(err, server) { + return cb(err, server); }); + }; + + beforeEach(function() { + reqPrivKey = new Bitcore.PrivateKey(); + var requestPubKey = reqPrivKey.toPublicKey(); + + var xPrivKey = TestData.copayers[0].xPrivKey_45H; + var sig = WalletUtils.signRequestPubKey(requestPubKey, xPrivKey); + + var copayerId = WalletUtils.xPubToCopayerId(TestData.copayers[0].xPubKey_45H); + opts = { + copayerId: copayerId, + requestPubKey: requestPubKey, + signature: sig, + }; + ws = new WalletService(); }); + describe('#addAccess 1-1', function() { + beforeEach(function(done) { + helpers.createAndJoinWallet(1, 1, function(s, w) { + server = s; + wallet = w; + helpers.stubUtxos(server, wallet, 1, function() { + done(); + }); + }); + }); - it('#addCopayerRequestKey', function(done) { - helpers.stubUtxos(server, wallet, [1, 'u2', 3], function() { - server.getBalance({}, function(err, balance) { + it('should be able to re-gain access from xPrivKey', function(done) { + ws.addAccess(opts, function(err, res) { should.not.exist(err); - should.exist(balance); - balance.totalAmount.should.equal(helpers.toSatoshi(6)); + res.wallet.copayers[0].requestPubKeys.length.should.equal(2); + res.wallet.copayers[0].requestPubKeys[0].selfSigned.should.equal(true); + + server.getBalance(res.wallet.walletId, function(err, bal) { + should.not.exist(err); + bal.totalAmount.should.equal(1e8); + getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) { + server2.getBalance(res.wallet.walletId, function(err, bal2) { + should.not.exist(err); + bal2.totalAmount.should.equal(1e8); + done(); + }); + }); + }); + }); + }); + + it('should fail to gain access with wrong xPrivKey', function(done) { + opts.signature = 'xx'; + ws.addAccess(opts, function(err, res) { + err.code.should.equal('NOT_AUTHORIZED'); done(); }); }); + + it('should fail to access with wrong privkey after gaining access', function(done) { + ws.addAccess(opts, function(err, res) { + should.not.exist(err); + server.getBalance(res.wallet.walletId, function(err, bal) { + should.not.exist(err); + var privKey = new Bitcore.PrivateKey(); + (getAuthServer(opts.copayerId, privKey, function(err, server2) { + err.code.should.equal('NOT_AUTHORIZED'); + done(); + })); + }); + }); + }); + + it('should be able to create TXs after regaining access', function(done) { + ws.addAccess(opts, function(err, res) { + should.not.exist(err); + getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) { + var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, null, reqPrivKey); + server2.createTx(txOpts, function(err, tx) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + + describe('#addAccess 2-2', function() { + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 2, function(s, w) { + server = s; + wallet = w; + helpers.stubUtxos(server, wallet, 1, function() { + done(); + }); + }); + }); + + it('should be able to re-gain access from xPrivKey', function(done) { + ws.addAccess(opts, function(err, res) { + should.not.exist(err); + server.getBalance(res.wallet.walletId, function(err, bal) { should.not.exist(err); + bal.totalAmount.should.equal(1e8); + getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) { + server2.getBalance(res.wallet.walletId, function(err, bal2) { + should.not.exist(err); + bal2.totalAmount.should.equal(1e8); + done(); + }); + }); + }); + }); + }); + + it('TX proposals should include info to be verified', function(done) { + ws.addAccess(opts, function(err, res) { + should.not.exist(err); + getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) { + var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, null, reqPrivKey); + server2.createTx(txOpts, function(err, tx) { + should.not.exist(err); + server2.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + should.exist(txs[0].proposalSignaturePubKey); + should.exist(txs[0].proposalSignaturePubKeySig); + done(); + }); + }); + }); + }); + }); + }); }); - describe('#getBalance', function() { var server, wallet; beforeEach(function(done) { @@ -4141,7 +4265,7 @@ describe('Wallet service', function() { }); }); }); - + describe('Legacy', function() { describe('Fees', function() { var server, wallet; @@ -4308,7 +4432,6 @@ describe('Wallet service', function() { }); }); }); - }); }); });