diff --git a/bit-wallet/bit-sign b/bit-wallet/bit-sign index 9a898f5..34988c7 100755 --- a/bit-wallet/bit-sign +++ b/bit-wallet/bit-sign @@ -1,12 +1,15 @@ #!/usr/bin/env node var _ = require('lodash'); +var fs = require('fs'); var program = require('commander'); -var utils = require('./cli-utils'); +var utils = require('./cli-utils'); program = utils.configureCommander(program); program .usage('[options] ') + .option('-i, --input [filename]', 'use signatures from file') + .option('-o, --output [filename]', 'write signatures to file') .parse(process.argv); var args = program.args; @@ -17,11 +20,33 @@ client.getTxProposals({}, function(err, txps) { utils.die(err); var txp = utils.findOneTxProposal(txps, txpid); - client.signTxProposal(txp, function(err, tx) { - utils.die(err); - if (tx.status == 'broadcasted') - console.log('Transaction Broadcasted: TXID: ' + tx.txid); - else - console.log('Transaction signed by you.'); - }); + + if (program.output) { + client.getSignatures(txp, function(err, signatures) { + utils.die(err); + var out = { + id: txp.id, + signatures: signatures, + }; + fs.writeFileSync(program.output, JSON.stringify(out)); + console.log('Signatures written to file.'); + }); + } else { + + if (program.input) { + var infile = JSON.parse(fs.readFileSync(program.input)); + if (infile.id != txp.id) + utils.die('Signatures does not match Transaction') + + txp.signatures = infile.signatures; + } + + client.signTxProposal(txp, function(err, tx) { + utils.die(err); + if (tx.status == 'broadcasted') + console.log('Transaction Broadcasted: TXID: ' + tx.txid); + else + console.log('Transaction signed by you.'); + }); + } }); diff --git a/lib/client/api.js b/lib/client/api.js index da9d3df..b30a75b 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -579,6 +579,57 @@ API.prototype.getTxProposals = function(opts, cb) { }); }; +API.prototype._getSignaturesFor = function(txp, data) { + + //Derive proper key to sign, for each input + var privs = [], + derived = {}; + + var network = new Bitcore.Address(txp.toAddress).network.name; + var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network); + + _.each(txp.inputs, function(i) { + if (!derived[i.path]) { + derived[i.path] = xpriv.derive(i.path).privateKey; + privs.push(derived[i.path]); + } + }); + + var t = new Bitcore.Transaction(); + + _.each(txp.inputs, function(i) { + t.from(i, i.publicKeys, txp.requiredSignatures); + }); + + t.to(txp.toAddress, txp.amount) + .change(txp.changeAddress.address); + + var signatures = _.map(privs, function(priv, i) { + return t.getSignatures(priv); + }); + + signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { + return s.signature.toDER().toString('hex'); + }); + + return signatures; +}; + +API.prototype.getSignatures = function(txp, cb) { + $.checkArgument(txp.creatorId); + var self = this; + + this._loadAndCheck(function(err, data) { + if (err) return cb(err); + + if (!Verifier.checkTxProposal(data, txp)) { + return cb(new ServerCompromisedError('Transaction proposal is invalid')); + } + + return cb(null, self._getSignaturesFor(txp, data)); + }); +}; + API.prototype.signTxProposal = function(txp, cb) { $.checkArgument(txp.creatorId); @@ -591,36 +642,7 @@ API.prototype.signTxProposal = function(txp, cb) { return cb(new ServerCompromisedError('Server sent fake transaction proposal')); } - //Derive proper key to sign, for each input - var privs = [], - derived = {}; - - var network = new Bitcore.Address(txp.toAddress).network.name; - var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network); - - _.each(txp.inputs, function(i) { - if (!derived[i.path]) { - derived[i.path] = xpriv.derive(i.path).privateKey; - privs.push(derived[i.path]); - } - }); - - var t = new Bitcore.Transaction(); - - _.each(txp.inputs, function(i) { - t.from(i, i.publicKeys, txp.requiredSignatures); - }); - - t.to(txp.toAddress, txp.amount) - .change(txp.changeAddress.address); - - var signatures = _.map(privs, function(priv, i) { - return t.getSignatures(priv); - }); - - signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { - return s.signature.toDER().toString('hex'); - }); + var signatures = txp.signatures || self._getSignaturesFor(txp, data); var url = '/v1/txproposals/' + txp.id + '/signatures/'; var args = { diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 7de8172..a1df273 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -353,7 +353,7 @@ describe('client API ', function() { }); }); - describe('Access control & export', function() { + describe('Access control', function() { it('should not be able to create address if not rwPubKey', function(done) { helpers.createAndJoinWallet(clients, 1, 1, function(err) { should.not.exist(err); @@ -438,7 +438,8 @@ describe('client API ', function() { }); }); }); - + }); + describe('Air gapped flows', function() { it('should be able get Tx proposals from a file', function(done) { helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { should.not.exist(err); @@ -499,9 +500,43 @@ describe('client API ', function() { }); }); }); + it('should be able export signatures and sign later from a ro client', + function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + should.not.exist(err); + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 1, 1); + blockExplorerMock.setUtxo(x0, 1, 2); + var opts = { + amount: 150000000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hello 1-1', + }; + clients[0].sendTxProposal(opts, function(err, txp) { + should.not.exist(err); + clients[0].getSignatures(txp, function(err, signatures) { + should.not.exist(err); + signatures.length.should.equal(txp.inputs.length); + signatures[0].length.should.above(62 * 2); + txp.signatures = signatures; + // Make client RO + var data = JSON.parse(fsmock._get('client0')); + delete data.xPrivKey; + fsmock._set('client0', JSON.stringify(data)); + clients[0].signTxProposal(txp, function(err, txp) { + should.not.exist(err); + txp.status.should.equal('broadcasted'); + done(); + }); + }); + }); + }); + }); + }); }); describe('Address Creation', function() { @@ -887,7 +922,9 @@ describe('client API ', function() { should.not.exist(err); clients[0].createAddress(function(err, x0) { should.not.exist(err); - clients[0].getMainAddresses({doNotVerify: true}, function(err, addr) { + clients[0].getMainAddresses({ + doNotVerify: true + }, function(err, addr) { should.not.exist(err); addr.length.should.equal(2); done(); @@ -940,9 +977,20 @@ describe('client API ', function() { }; clients[0].sendTxProposal(opts, function(err, x) { should.not.exist(err); + clients[0].getStatus( function(err, st) { + should.not.exist(err); + var x = st.pendingTxps[0]; x.status.should.equal('pending'); x.requiredRejections.should.equal(2); x.requiredSignatures.should.equal(2); + var w = st.wallet; + w.copayers.length.should.equal(3); + w.status.should.equal('complete'); + var b = st.balance; + b.totalAmount.should.equal(1000000000); + b.lockedAmount.should.equal(1000000000); + + clients[0].signTxProposal(x, function(err, tx) { should.not.exist(err, err); tx.status.should.equal('pending'); @@ -952,6 +1000,7 @@ describe('client API ', function() { tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id); done(); }); + }); }); }); });