diff --git a/lib/model/copayer.js b/lib/model/copayer.js index fb5d504..005ece9 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -1,6 +1,11 @@ 'use strict'; var _ = require('lodash'); +var Bitcore = require('bitcore'); +var HDPublicKey = Bitcore.HDPublicKey; + +var VERSION = '1.0.0'; +var MESSAGE_SIGNING_PATH = "m/1/0"; function Copayer(opts) { opts = opts || {}; @@ -10,16 +15,28 @@ function Copayer(opts) { this.name = opts.name; this.xPubKey = opts.xPubKey; this.xPubKeySignature = opts.xPubKeySignature; + this.version = VERSION; + this.signingPubKey = opts.signingPubKey || this.getSigningPubKey(); + +}; + + +Copayer.prototype.getSigningPubKey = function () { + if (!this.xPubKey) return null; + return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString(); }; Copayer.fromObj = function (obj) { var x = new Copayer(); + x.createdOn = obj.createdOn; x.id = obj.id; x.name = obj.name; x.xPubKey = obj.xPubKey; x.xPubKeySignature = obj.xPubKeySignature; + x.signingPubKey = obj.signingPubKey || this.getSigningPubKey(); + return x; }; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index 792cb07..d83cdc8 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -25,13 +25,19 @@ function Wallet(opts) { this.pubKey = new PublicKey(opts.pubKey); }; -Wallet.fromObj = function (obj) { - var x = new Wallet(); + +Wallet.fromUntrustedObj = function (obj) { // TODO add sanity checks OR migration steps? if (!obj.pubKey || !obj.m || !obj.n) return cb('Wallet corrupted'); + return Wallet.fromObj(obj); +}; + +Wallet.fromObj = function (obj) { + var x = new Wallet(); + x.createdOn = obj.createdOn; x.id = obj.id; x.name = obj.name; diff --git a/lib/server.js b/lib/server.js index dfb3710..49083e2 100644 --- a/lib/server.js +++ b/lib/server.js @@ -7,6 +7,7 @@ var log = require('npmlog'); log.debug = log.verbose; var Bitcore = require('bitcore'); var PublicKey = Bitcore.PublicKey; +var HDPublicKey = Bitcore.HDPublicKey; var Explorers = require('bitcore-explorers'); var Lock = require('./lock'); @@ -18,6 +19,7 @@ var Copayer = require('./model/copayer'); var Address = require('./model/address'); var TxProposal = require('./model/txproposal'); + /** * Creates an instance of the Copay server. * @constructor @@ -196,14 +198,11 @@ CopayServer.prototype.verifyMessageSignature = function (opts, cb) { var copayer = wallet.getCopayer(opts.copayerId); if (!copayer) return cb('Copayer not found'); - var isValid = self._doVerifyMessageSignature(copayer.xPubKey, opts.message, opts.signature); + var isValid = self._verifySignature(opts.message, opts.signature, copayer.signingPubKey); return cb(null, isValid); }); }; -CopayServer.prototype._doVerifyMessageSignature = function (pubKey, message, signature) { - throw 'not implemented'; -}; CopayServer.prototype._getBlockExplorer = function (provider, network) { var url; diff --git a/test/integration.js b/test/integration.js index 9f45c15..74a5116 100644 --- a/test/integration.js +++ b/test/integration.js @@ -16,43 +16,84 @@ var Address = require('../lib/model/address'); var Copayer = require('../lib/model/copayer'); var CopayServer = require('../lib/server'); +var keyPair = { + priv: '0dea92f1df6675085b5cdd965487bb862f84f2755bcb56fa45dbf5b387a6c4a0', + pub: '026092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80', +}; + + var aPubKey = '042F65F56A6C06C2B651C473AC221B2460DA57859AFB72564E9781B655EBC0AFAF322B9A732324ECC92A3319DFB1F0D53F0CB7E6620C98BD1EF53106A7CF3F6DB9'; +var aXPubKey = 'xpub661MyMwAqRbcFHFFvUP6HaKdd2FYzNcZCGagxMzQEf1J3x2DeASBW2JWox7ToGwPM7V2yRzQAxcD6MdPid9C8kwhKkVWBxQ3dMo8zu3pub7'; +var aXPubKeySignature = '3045022100f988737147894bbfdc196c1289e4d970b391c0d8e9d1fcc0397f16e6a31c9df2022014d9af9aceccb540f4a5a2680e2aebb1f3df55bcf3778599b78314a02064c592'; // with keyPair.priv + +// Copayers + +var someXPrivKey = [ + 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e', +]; + +var someXPubKeys = [ + 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', + 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', + 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', + 'xpub661MyMwAqRbcGpExxHEzAWxBQX3k76NyerSpjqucSXXfTqH6Wq9sUVRwTjpHZHwapDbG16KEB9w9r3LT2jKYqU9xJf1YBAaZFikbUHiV1tg', + 'xpub661MyMwAqRbcEvKQnt9ELHHcangXssm174sWr5gNTSmQYsAtvQJNUpLETDTm1vDxwtABvB4SRjGkNMm37NnMerKg4e3ygqmWEr75Fka4dK7', + 'xpub661MyMwAqRbcG67ioS7rz3fFg7EDQNLJ9m1etAPwBecZhL5kKAKe4JU5jCTzRcEWp28XCYA1gKh7jyficSr97gcR2pjDL5jbWua1CwTKWV4', +]; + + +var someXPubKeysSignatures = [ + '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46', + '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62', + '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c', + '304402203ae5bf7fa8935b8ab2ac33724dbb191356cecb47c8371d2c9389e918a3600918022073b48705306730c8fe4ab22d5f6ed3ca3def27eb6e8c5cc8f53e23c11fa5e5ef', + '3045022100eabd2a605403b377a8db9eec57726da0309a7eb385e7e4e5273b9862046f25ef02204d18755a90580a98f45e162ae5d5dc39aa3aa708a0d79433ed259e70a832b49c', + '3045022100c282254773c65025054e18a61ee550cbf78b88fc72ef66770050815b62502d9c02206e0df528203c9201c144f865df71f5d2471668f4ed8387979fcee20f6fa121a9', +]; // with keyPair.priv + +//Copayer signature +var aText = 'hello world'; +var aTextSignature = '3045022100addd20e5413865d65d561ad2979f2289a40d52594b1f804840babd9a63e4ebbf02204b86285e1fcab02df772e7a1325fc4b511ecad79a8f80a2bd1ad8bfa858ac3d4'; // with someXPrivKey[0].derive('m/1/0')=5c0e043a513032907d181325a8e7990b076c0af15ed13dc5e611cda9bb3ae52a; + var helpers = {}; -helpers.createAndJoinWallet = function (id, m, n, cb) { +helpers.createAndJoinWallet = function(id, m, n, cb) { var walletOpts = { id: id, name: id + ' wallet', m: m, n: n, - pubKey: aPubKey, + pubKey: keyPair.pub, }; server.createWallet(walletOpts, function(err) { if (err) return cb(err); - async.each(_.range(1, n + 1), function (i, cb) { + async.each(_.range(1, n + 1), function(i, cb) { var copayerOpts = { walletId: id, id: '' + i, name: 'copayer ' + i, - xPubKey: 'dummy' + i, - xPubKeySignature: 'dummy', + xPubKey: someXPubKeys[i - 1], + xPubKeySignature: someXPubKeysSignatures[i - 1], }; - server.joinWallet(copayerOpts, function (err) { + server.joinWallet(copayerOpts, function(err) { return cb(err); }); - }, function (err) { + }, function(err) { if (err) return cb(err); - server.getWallet({ id: id, includeCopayers: true }, function (err, wallet) { + server.getWallet({ + id: id, + includeCopayers: true + }, function(err, wallet) { return cb(err, wallet); }); }); }); }; -helpers.createUtxos = function (amounts) { +helpers.createUtxos = function(amounts) { amounts = [].concat(amounts); - return _.map(amounts, function (amount) { + return _.map(amounts, function(amount) { return { txid: 'dummy' + Math.random(), vout: Math.floor((Math.random() * 10) + 1), @@ -67,8 +108,12 @@ var server; describe('Copay server', function() { beforeEach(function() { - db = levelup(memdown, { valueEncoding: 'json' }); - storage = new Storage({ db: db }); + db = levelup(memdown, { + valueEncoding: 'json' + }); + storage = new Storage({ + db: db + }); }); describe('#getWallet', function() { @@ -78,7 +123,7 @@ describe('Copay server', function() { }); }); - it('should get existing wallet', function (done) { + it('should get existing wallet', function(done) { var w1 = new Wallet({ id: '123', @@ -106,7 +151,10 @@ describe('Copay server', function() { value: w2, }]); - server.getWallet({ id: '123', includeCopayers: true }, function (err, wallet) { + server.getWallet({ + id: '123', + includeCopayers: true + }, function(err, wallet) { should.not.exist(err); wallet.id.should.equal('123'); wallet.name.should.equal('my wallet'); @@ -116,7 +164,7 @@ describe('Copay server', function() { }); }); - it('should fail when requesting non-existent wallet', function (done) { + it('should fail when requesting non-existent wallet', function(done) { var w1 = new Wallet({ id: '123', name: 'my wallet', @@ -141,7 +189,9 @@ describe('Copay server', function() { value: w2, }]); - server.getWallet({ id: '345' }, function (err, wallet) { + server.getWallet({ + id: '345' + }, function(err, wallet) { should.exist(err); err.should.equal('Wallet not found'); done(); @@ -166,7 +216,9 @@ describe('Copay server', function() { }; server.createWallet(opts, function(err) { should.not.exist(err); - server.getWallet({ id: '123' }, function (err, wallet) { + server.getWallet({ + id: '123' + }, function(err, wallet) { should.not.exist(err); wallet.id.should.equal('123'); wallet.name.should.equal('my wallet'); @@ -185,7 +237,9 @@ describe('Copay server', function() { }; server.createWallet(opts, function(err) { should.not.exist(err); - server.getWallet({ id: '123' }, function (err, wallet) { + server.getWallet({ + id: '123' + }, function(err, wallet) { should.not.exist(err); wallet.id.should.equal('123'); wallet.name.should.equal('my wallet'); @@ -203,29 +257,32 @@ describe('Copay server', function() { server = new CopayServer({ storage: storage, }); - server._verifySignature = sinon.stub().returns(true); }); - it('should join existing wallet', function (done) { + it('should join existing wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 2, n: 3, - pubKey: aPubKey, + pubKey: keyPair.pub, }; + server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayerOpts = { walletId: '123', id: '999', name: 'me', - xPubKey: 'dummy', - xPubKeySignature: 'dummy', + xPubKey: aXPubKey, + xPubKeySignature: aXPubKeySignature, }; - server.joinWallet(copayerOpts, function (err) { + server.joinWallet(copayerOpts, function(err) { should.not.exist(err); - server.getWallet({ id: '123', includeCopayers: true }, function (err, wallet) { + server.getWallet({ + id: '123', + includeCopayers: true + }, function(err, wallet) { wallet.id.should.equal('123'); wallet.copayers.length.should.equal(1); var copayer = wallet.copayers[0]; @@ -237,7 +294,7 @@ describe('Copay server', function() { }); }); - it('should fail to join non-existent wallet', function (done) { + it('should fail to join non-existent wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', @@ -254,20 +311,20 @@ describe('Copay server', function() { xPubKey: 'dummy', xPubKeySignature: 'dummy', }; - server.joinWallet(copayerOpts, function (err) { + server.joinWallet(copayerOpts, function(err) { should.exist(err); done(); }); }); }); - it('should fail to join full wallet', function (done) { + it('should fail to join full wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 1, n: 1, - pubKey: aPubKey, + pubKey: keyPair.pub, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); @@ -275,21 +332,23 @@ describe('Copay server', function() { walletId: '123', id: '111', name: 'me', - xPubKey: 'dummy1', - xPubKeySignature: 'dummy', + xPubKey: someXPubKeys[0], + xPubKeySignature: someXPubKeysSignatures[0], }; var copayer2Opts = { walletId: '123', id: '222', name: 'me 2', - xPubKey: 'dummy2', - xPubKeySignature: 'dummy', + xPubKey: someXPubKeys[1], + xPubKeySignature: someXPubKeysSignatures[1], }; - server.joinWallet(copayer1Opts, function (err) { + server.joinWallet(copayer1Opts, function(err) { should.not.exist(err); - server.getWallet({ id: '123' }, function (err, wallet) { + server.getWallet({ + id: '123' + }, function(err, wallet) { wallet.status.should.equal('complete'); - server.joinWallet(copayer2Opts, function (err) { + server.joinWallet(copayer2Opts, function(err) { should.exist(err); err.should.equal('Wallet full'); done(); @@ -299,13 +358,13 @@ describe('Copay server', function() { }); }); - it('should fail to re-join wallet', function (done) { + it('should fail to re-join wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 1, n: 1, - pubKey: aPubKey, + pubKey: keyPair.pub, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); @@ -313,12 +372,12 @@ describe('Copay server', function() { walletId: '123', id: '111', name: 'me', - xPubKey: 'dummy', - xPubKeySignature: 'dummy', + xPubKey: someXPubKeys[0], + xPubKeySignature: someXPubKeysSignatures[0], }; - server.joinWallet(copayerOpts, function (err) { + server.joinWallet(copayerOpts, function(err) { should.not.exist(err); - server.joinWallet(copayerOpts, function (err) { + server.joinWallet(copayerOpts, function(err) { should.exist(err); err.should.equal('Copayer already in wallet'); done(); @@ -327,16 +386,93 @@ describe('Copay server', function() { }); }); - it('should set pkr and status = complete on last copayer joining', function (done) { - helpers.createAndJoinWallet('123', 2, 3, function (err, wallet) { - server.getWallet({ id: '123' }, function (err, wallet) { + + it('should fail to join with bad formated signature', function(done) { + var walletOpts = { + id: '123', + name: 'my wallet', + m: 1, + n: 1, + pubKey: aPubKey, + }; + server.createWallet(walletOpts, function(err) { + should.not.exist(err); + var copayerOpts = { + walletId: '123', + id: '111', + name: 'me', + xPubKey: someXPubKeys[0], + xPubKeySignature: 'bad sign', + }; + server.joinWallet(copayerOpts, function(err) { + err.should.contain('Bad request'); + done(); + }); + }); + }); + + + it('should fail to join with null signature', function(done) { + var walletOpts = { + id: '123', + name: 'my wallet', + m: 1, + n: 1, + pubKey: aPubKey, + }; + server.createWallet(walletOpts, function(err) { + should.not.exist(err); + var copayerOpts = { + walletId: '123', + id: '111', + name: 'me', + xPubKey: someXPubKeys[0], + }; + server.joinWallet(copayerOpts, function(err) { + err.should.contain('Bad request'); + done(); + }); + }); + }); + + it('should fail to join with wrong signature', function(done) { + var walletOpts = { + id: '123', + name: 'my wallet', + m: 1, + n: 1, + pubKey: aPubKey, + }; + server.createWallet(walletOpts, function(err) { + should.not.exist(err); + var copayerOpts = { + walletId: '123', + id: '111', + name: 'me', + xPubKey: someXPubKeys[0], + xPubKeySignature: someXPubKeysSignatures[0], + }; + server.joinWallet(copayerOpts, function(err) { + err.should.contain('Bad request'); + done(); + }); + }); + }); + + + + it('should set pkr and status = complete on last copayer joining', function(done) { + helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) { + server.getWallet({ + id: '123' + }, function(err, wallet) { should.not.exist(err); wallet.status.should.equal('complete'); wallet.publicKeyRing.length.should.equal(3); done(); }); }); - }); + }); }); @@ -348,32 +484,31 @@ describe('Copay server', function() { }); }); - it('should successfully verify message signature', function (done) { - server._doVerifyMessageSignature = sinon.stub().returns(true); - helpers.createAndJoinWallet('123', 2, 2, function (err, wallet) { + it('should successfully verify message signature', function(done) { + helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { var opts = { walletId: '123', copayerId: '1', - message: 'hello world', - signature: 'dummy', + message: aText, + signature: aTextSignature, }; - server.verifyMessageSignature(opts, function (err, isValid) { + server.verifyMessageSignature(opts, function(err, isValid) { should.not.exist(err); - isValid.should.be.true; + isValid.should.equal(true); done(); }); }); }); - it('should fail to verify message signature when copayer does not exist', function (done) { - helpers.createAndJoinWallet('123', 2, 2, function (err, wallet) { + it('should fail to verify message signature when copayer does not exist', function(done) { + helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { var opts = { walletId: '123', copayerId: '999', message: 'hello world', signature: 'dummy', }; - server.verifyMessageSignature(opts, function (err, isValid) { + server.verifyMessageSignature(opts, function(err, isValid) { err.should.equal('Copayer not found'); done(); }); @@ -388,10 +523,15 @@ describe('Copay server', function() { }); }); - it('should create address', function (done) { - server._doCreateAddress = sinon.stub().returns(new Address({ address: 'addr1', path: 'path1' })); - helpers.createAndJoinWallet('123', 2, 2, function (err, wallet) { - server.createAddress({ walletId: '123' }, function (err, address) { + it('should create address', function(done) { + server._doCreateAddress = sinon.stub().returns(new Address({ + address: 'addr1', + path: 'path1' + })); + helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { + server.createAddress({ + walletId: '123' + }, function(err, address) { should.not.exist(err); address.should.exist; address.address.should.equal('addr1'); @@ -407,15 +547,20 @@ describe('Copay server', function() { server = new CopayServer({ storage: storage, }); - server._doCreateAddress = sinon.stub().returns(new Address({ address: 'addr1', path: 'path1' })); - helpers.createAndJoinWallet('123', 2, 2, function (err, wallet) { - server.createAddress({ walletId: '123' }, function (err, address) { + server._doCreateAddress = sinon.stub().returns(new Address({ + address: 'addr1', + path: 'path1' + })); + helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { + server.createAddress({ + walletId: '123' + }, function(err, address) { done(); }); }); }); - it('should create tx', function (done) { + it('should create tx', function(done) { var bc = sinon.stub(); bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, helpers.createUtxos([100, 200])); server._getBlockExplorer = sinon.stub().returns(bc); @@ -431,16 +576,20 @@ describe('Copay server', function() { otToken: 'dummy', requestSignature: 'dummy', }; - server.createTx(txOpts, function (err, tx) { + server.createTx(txOpts, function(err, tx) { should.not.exist(err); tx.should.exist; tx.rawTx.should.equal('raw'); tx.isAccepted().should.equal.false; tx.isRejected().should.equal.false; - server.getPendingTxs({ walletId: '123' }, function (err, txs) { + server.getPendingTxs({ + walletId: '123' + }, function(err, txs) { should.not.exist(err); txs.length.should.equal(1); - server.getBalance({ walletId: '123' }, function (err, balance) { + server.getBalance({ + walletId: '123' + }, function(err, balance) { should.not.exist(err); balance.totalAmount.should.equal(300); balance.lockedAmount.should.equal(200); @@ -450,13 +599,10 @@ describe('Copay server', function() { }); }); - it.skip('should fail to create tx when insufficient funds', function (done) { - }); + it.skip('should fail to create tx when insufficient funds', function(done) {}); - it.skip('should create tx when there is a pending tx and enough UTXOs', function (done) { - }); + it.skip('should create tx when there is a pending tx and enough UTXOs', function(done) {}); - it.skip('should fail to create tx when there is a pending tx and not enough UTXOs', function (done) { - }); + it.skip('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) {}); }); });