From 879a352b3e49df89fd53928fc7c19ca90aad16dc Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 23 Feb 2015 00:43:39 -0300 Subject: [PATCH 1/7] add public key ring to txprposals --- bit-wallet/bit-txproposals | 11 ++- lib/client/api.js | 80 ++++++++++++++------- lib/client/verifier.js | 2 + test/integration/clientApi.js | 128 ++++++++++++++++++++++------------ 4 files changed, 150 insertions(+), 71 deletions(-) diff --git a/bit-wallet/bit-txproposals b/bit-wallet/bit-txproposals index bb64bc4..7dd298e 100755 --- a/bit-wallet/bit-txproposals +++ b/bit-wallet/bit-txproposals @@ -23,8 +23,15 @@ function end(err, txps, rawtxps) { } utils.renderTxProposals(txps); if (program.output) { - fs.writeFileSync(program.output, JSON.stringify(rawtxps)); - console.log(' * Proposals Saved to: %s\n', program.output); + + client.getEncryptedPublicKeyRing(function (err, pkr) { + var txData = { + pkr: pkr, + txps: txps, + }; + fs.writeFileSync(program.output, JSON.stringify(txData)); + console.log(' * Proposals Saved to: %s\n', program.output); + }); } }; diff --git a/lib/client/api.js b/lib/client/api.js index b30a75b..77c4a17 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -83,8 +83,7 @@ function API(opts) { } }; - -API.prototype._tryToComplete = function(data, cb) { +API.prototype._tryToCompleteFromServer = function(data, cb) { var self = this; var url = '/v1/wallets/'; @@ -112,6 +111,24 @@ API.prototype._tryToComplete = function(data, cb) { +API.prototype._tryToComplete = function(opts, data, cb) { + if (opts.pkr) { + var pkr = _decryptMessage(opts.pkr, data.sharedEncryptingKey); + + if (!pkr) + return cb('Could not complete wallet'); + + data.publicKeyRing = JSON.parse(pkr); + this.storage.save(data, function(err) { + return cb(err, data); + }); + } else { + this._tryToCompleteFromServer(data,cb); + } +}; + + + API.prototype._load = function(cb) { var self = this; @@ -124,7 +141,12 @@ API.prototype._load = function(cb) { }; -API.prototype._loadAndCheck = function(cb) { +/** + * _loadAndCheck + * + * @param opts.pkr + */ +API.prototype._loadAndCheck = function(opts, cb) { var self = this; this._load(function(err, data) { @@ -133,7 +155,7 @@ API.prototype._loadAndCheck = function(cb) { var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; if (!pkrComplete) { - return self._tryToComplete(data, cb); + return self._tryToComplete(opts, data, cb); } } return cb(null, data); @@ -270,7 +292,7 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb API.prototype.reCreateWallet = function(walletName, cb) { var self = this; - this._loadAndCheck(function(err, data) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); var walletPrivKey = new Bitcore.PrivateKey(); @@ -351,7 +373,7 @@ API.prototype.sendTxProposal = function(opts, cb) { var self = this; - this._loadAndCheck(function(err, data) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); if (!data.rwPrivKey) @@ -374,7 +396,7 @@ API.prototype.sendTxProposal = function(opts, 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/'; @@ -396,7 +418,7 @@ API.prototype.createAddress = function(cb) { API.prototype.getMainAddresses = function(opts, cb) { var self = this; - this._loadAndCheck(function(err, data) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); var url = '/v1/addresses/'; @@ -422,7 +444,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); @@ -440,7 +462,7 @@ API.prototype.export = function(opts, cb) { opts = opts || {}; var access = opts.access || 'full'; - this._loadAndCheck(function(err, data) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); var v = []; @@ -517,19 +539,12 @@ API.prototype.import = function(str, cb) { * */ -API.prototype.parseTxProposals = function(txps, cb) { +API.prototype.parseTxProposals = function(txData, cb) { var self = this; - this._load(function(err, data) { - if (err) return cb(err); - if (data.n > 1) { - var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; - if (!pkrComplete) { - return cb('Wallet Incomplete'); - } - } - + this._loadAndCheck({pkr: txData.pkr},function(err, data) { + var txps = txData.txps; _processTxps(txps, data.sharedEncryptingKey); var fake = _.any(txps, function(txp) { @@ -555,7 +570,7 @@ API.prototype.parseTxProposals = function(txps, cb) { API.prototype.getTxProposals = function(opts, cb) { var self = this; - this._loadAndCheck(function(err, data) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); var url = '/v1/txproposals/'; self._doGetRequest(url, data, function(err, txps) { @@ -619,7 +634,7 @@ API.prototype.getSignatures = function(txp, cb) { $.checkArgument(txp.creatorId); var self = this; - this._loadAndCheck(function(err, data) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); if (!Verifier.checkTxProposal(data, txp)) { @@ -630,12 +645,25 @@ API.prototype.getSignatures = function(txp, cb) { }); }; +API.prototype.getEncryptedPublicKeyRing = function(cb) { + var self = this; + + this._loadAndCheck({}, function(err, data) { + if (err) return cb(err); + + var pkr = JSON.stringify(data.publicKeyRing); + return cb(null, _encryptMessage(pkr, data.sharedEncryptingKey)); + }); +}; + + + API.prototype.signTxProposal = function(txp, cb) { $.checkArgument(txp.creatorId); var self = this; - this._loadAndCheck(function(err, data) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); if (!Verifier.checkTxProposal(data, txp)) { @@ -658,7 +686,7 @@ API.prototype.rejectTxProposal = function(txp, reason, cb) { var self = this; - this._loadAndCheck( + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); @@ -673,7 +701,7 @@ API.prototype.rejectTxProposal = function(txp, reason, cb) { API.prototype.broadcastTxProposal = function(txp, cb) { var self = this; - this._loadAndCheck( + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); @@ -686,7 +714,7 @@ API.prototype.broadcastTxProposal = function(txp, cb) { API.prototype.removeTxProposal = function(txp, cb) { var self = this; - this._loadAndCheck( + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); var url = '/v1/txproposals/' + txp.id; diff --git a/lib/client/verifier.js b/lib/client/verifier.js index 1b2a324..f1a4897 100644 --- a/lib/client/verifier.js +++ b/lib/client/verifier.js @@ -59,7 +59,9 @@ Verifier.checkTxProposal = function(data, txp) { var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) { if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true; }); + if (!creatorXPubKey) return false; + var creatorSigningPubKey = (new Bitcore.HDPublicKey(creatorXPubKey)).derive('m/1/1').publicKey.toString(); var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.encryptedMessage || txp.message); diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index a1df273..63a7838 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -458,7 +458,9 @@ describe('client API ', function() { }, function(err, txs, rawTxps) { should.not.exist(err); - clients[0].parseTxProposals(rawTxps, function(err, txs2) { + clients[0].parseTxProposals({ + txps: rawTxps + }, function(err, txs2) { should.not.exist(err); txs[0].should.deep.equal(txs2[0]); done(); @@ -490,7 +492,9 @@ describe('client API ', function() { //Tamper rawTxps[0].amount++; - clients[0].parseTxProposals(rawTxps, function(err, txs2) { + clients[0].parseTxProposals({ + txps: rawTxps + }, function(err, txs2) { err.code.should.equal('SERVERCOMPROMISED'); done(); }); @@ -500,43 +504,81 @@ 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) { + + it('should complete public key ring from file', function(done) { + helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { should.not.exist(err); - clients[0].createAddress(function(err, x0) { + + clients[1].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 1, 1); - blockExplorerMock.setUtxo(x0, 1, 2); var opts = { - amount: 150000000, + amount: 10000000, toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', message: 'hello 1-1', }; - clients[0].sendTxProposal(opts, function(err, txp) { + clients[1].sendTxProposal(opts, function(err, x) { should.not.exist(err); - clients[0].getSignatures(txp, function(err, signatures) { + clients[1].getTxProposals({ + getRawTxps: true + }, function(err, txs, rawTxps) { 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) { + clients[1].getEncryptedPublicKeyRing(function(err, pkr) { should.not.exist(err); - txp.status.should.equal('broadcasted'); - done(); + + // Will trigger _tryToComplete and use pkr + // then, needs pkr to verify the txps + clients[0].parseTxProposals({ + txps: rawTxps, + pkr: pkr, + }, function(err, txs2) { + should.not.exist(err); + done(); + }); }); }); }); }); }); }); + 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() { @@ -977,29 +1019,29 @@ 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].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'); - clients[1].signTxProposal(x, function(err, tx) { - should.not.exist(err); - tx.status.should.equal('broadcasted'); - tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id); - done(); - }); + clients[0].signTxProposal(x, function(err, tx) { + should.not.exist(err, err); + tx.status.should.equal('pending'); + clients[1].signTxProposal(x, function(err, tx) { + should.not.exist(err); + tx.status.should.equal('broadcasted'); + tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id); + done(); + }); }); }); }); From 9a70047852b3a77278fc04603bd299f5126d5c02 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 23 Feb 2015 01:19:44 -0300 Subject: [PATCH 2/7] changes encryption of pkr to airgapped --- lib/client/api.js | 7 +++---- lib/walletutils.js | 5 +++++ test/integration/clientApi.js | 37 +++++++++++++++++++++++------------ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 77c4a17..ddab8e8 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -113,7 +113,7 @@ API.prototype._tryToCompleteFromServer = function(data, cb) { API.prototype._tryToComplete = function(opts, data, cb) { if (opts.pkr) { - var pkr = _decryptMessage(opts.pkr, data.sharedEncryptingKey); + var pkr = _decryptMessage(opts.pkr,WalletUtils.privateKeyToAESKey(data.roPrivKey)); if (!pkr) return cb('Could not complete wallet'); @@ -219,7 +219,7 @@ API.prototype._initData = function(network, walletPrivKey, m, n) { var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); var roPrivKey = xPrivKey.derive('m/1/0').privateKey; var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; - var sharedEncryptingKey = Bitcore.crypto.Hash.sha256(walletPrivKey.toBuffer()).slice(0, 16).toString('base64'); + var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); var copayerId = WalletUtils.xPubToCopayerId(xPubKey); var data = { @@ -650,9 +650,8 @@ API.prototype.getEncryptedPublicKeyRing = function(cb) { this._loadAndCheck({}, function(err, data) { if (err) return cb(err); - var pkr = JSON.stringify(data.publicKeyRing); - return cb(null, _encryptMessage(pkr, data.sharedEncryptingKey)); + return cb(null, _encryptMessage(pkr, WalletUtils.privateKeyToAESKey(data.roPrivKey))); }); }; diff --git a/lib/walletutils.js b/lib/walletutils.js index d2a87c5..d99fdf9 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -138,4 +138,9 @@ WalletUtils.decryptMessage = function(cyphertextJson, encryptingKey) { return sjcl.decrypt(key, cyphertextJson); }; +WalletUtils.privateKeyToAESKey = function(privKey) { + var pk = Bitcore.PrivateKey.fromString(privKey); + return Bitcore.crypto.Hash.sha256(pk.toBuffer()).slice(0, 16).toString('base64'); +}; + module.exports = WalletUtils; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 63a7838..30d816f 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -519,22 +519,35 @@ describe('client API ', function() { }; clients[1].sendTxProposal(opts, function(err, x) { should.not.exist(err); - clients[1].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - clients[1].getEncryptedPublicKeyRing(function(err, pkr) { + // Create the proxy, ro, connected, device (2) + clients[0].export({ + access: 'readonly' + }, function(err, str) { + should.not.exist(err); + clients[2].import(str, function(err, wallet) { should.not.exist(err); - // Will trigger _tryToComplete and use pkr - // then, needs pkr to verify the txps - clients[0].parseTxProposals({ - txps: rawTxps, - pkr: pkr, - }, function(err, txs2) { + clients[2].getTxProposals({ + getRawTxps: true + }, function(err, txs, rawTxps) { should.not.exist(err); - done(); + + clients[2].getEncryptedPublicKeyRing(function(err, pkr) { + should.not.exist(err); + + // Back to the air gapped + // + // Will trigger _tryToComplete and use pkr + // then, needs pkr to verify the txps + clients[0].parseTxProposals({ + txps: rawTxps, + pkr: pkr, + }, function(err, txs2) { + should.not.exist(err); + done(); + }); + }); }); }); }); From 6c123f5c2df70db33991eb60c1aaacca13f4e948 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 23 Feb 2015 02:19:23 -0300 Subject: [PATCH 3/7] add other needed data to be completed --- bit-wallet/bit-txproposals | 4 +-- lib/client/api.js | 54 +++++++++++++++++++++-------------- test/integration/clientApi.js | 14 +++++++-- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/bit-wallet/bit-txproposals b/bit-wallet/bit-txproposals index 7dd298e..26a27ae 100755 --- a/bit-wallet/bit-txproposals +++ b/bit-wallet/bit-txproposals @@ -24,9 +24,9 @@ function end(err, txps, rawtxps) { utils.renderTxProposals(txps); if (program.output) { - client.getEncryptedPublicKeyRing(function (err, pkr) { + client.getEncryptedWalletData(function (err, toComplete) { var txData = { - pkr: pkr, + toComplete: toComplete, txps: txps, }; fs.writeFileSync(program.output, JSON.stringify(txData)); diff --git a/lib/client/api.js b/lib/client/api.js index ddab8e8..cec3b83 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -18,6 +18,8 @@ var BASE_URL = 'http://localhost:3001/copay/api'; var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'publicKeyRing', 'sharedEncryptingKey']; var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; +var WALLET_AIRGAPPED_TOCOMPLETE = ['publicKeyRing', 'm', 'n', 'sharedEncryptingKey']; + function _encryptMessage(message, encryptingKey) { if (!message) return null; return WalletUtils.encryptMessage(message, encryptingKey); @@ -110,20 +112,30 @@ API.prototype._tryToCompleteFromServer = function(data, cb) { }; +API.prototype._tryToCompleteFromData = function(data, toComplete, cb) { + var inData = _decryptMessage(toComplete, + WalletUtils.privateKeyToAESKey(data.roPrivKey)); + if (!inData) + return cb('Could not complete wallet'); + + try { + inData = JSON.parse(inData); + _.extend(data, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); + } catch (ex) { + return cb(ex); + } + + this.storage.save(data, function(err) { + return cb(err, data); + }); +}; + API.prototype._tryToComplete = function(opts, data, cb) { - if (opts.pkr) { - var pkr = _decryptMessage(opts.pkr,WalletUtils.privateKeyToAESKey(data.roPrivKey)); - - if (!pkr) - return cb('Could not complete wallet'); - - data.publicKeyRing = JSON.parse(pkr); - this.storage.save(data, function(err) { - return cb(err, data); - }); + if (opts.toComplete) { + this._tryToCompleteFromData(data, opts.toComplete, cb); } else { - this._tryToCompleteFromServer(data,cb); + this._tryToCompleteFromServer(data, cb); } }; @@ -151,13 +163,10 @@ API.prototype._loadAndCheck = function(opts, cb) { this._load(function(err, data) { if (err) return cb(err); - if (data.n > 1) { - var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; - if (!pkrComplete) { - return self._tryToComplete(opts, data, cb); - } - } + if (!data.n || (data.n > 1 && data.publicKeyRing.length != data.n)) + return self._tryToComplete(opts, data, cb); + return cb(null, data); }); }; @@ -542,7 +551,10 @@ API.prototype.import = function(str, cb) { API.prototype.parseTxProposals = function(txData, cb) { var self = this; - this._loadAndCheck({pkr: txData.pkr},function(err, data) { + this._loadAndCheck({ + toComplete: txData.toComplete + }, function(err, data) { + if (err) return cb(err); var txps = txData.txps; _processTxps(txps, data.sharedEncryptingKey); @@ -645,13 +657,13 @@ API.prototype.getSignatures = function(txp, cb) { }); }; -API.prototype.getEncryptedPublicKeyRing = function(cb) { +API.prototype.getEncryptedWalletData = function(cb) { var self = this; this._loadAndCheck({}, function(err, data) { if (err) return cb(err); - var pkr = JSON.stringify(data.publicKeyRing); - return cb(null, _encryptMessage(pkr, WalletUtils.privateKeyToAESKey(data.roPrivKey))); + var toComplete = JSON.stringify(_.pick(data, WALLET_AIRGAPPED_TOCOMPLETE)); + return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(data.roPrivKey))); }); }; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 30d816f..6d09aef 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -533,16 +533,26 @@ describe('client API ', function() { }, function(err, txs, rawTxps) { should.not.exist(err); - clients[2].getEncryptedPublicKeyRing(function(err, pkr) { + + clients[2].getEncryptedWalletData(function(err, toComplete) { should.not.exist(err); + // Disable networking + clients[0].request = sinon.stub().yields('no network'); + + // Make client incomplete + var data = JSON.parse(fsmock._get('client0')); + delete data.n; + fsmock._set('client0', JSON.stringify(data)); + // Back to the air gapped // // Will trigger _tryToComplete and use pkr // then, needs pkr to verify the txps + clients[0].parseTxProposals({ txps: rawTxps, - pkr: pkr, + toComplete: toComplete, }, function(err, txs2) { should.not.exist(err); done(); From 53ca9b00fcff79fa4b13d273dfe3710915e29d4b Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 23 Feb 2015 02:33:43 -0300 Subject: [PATCH 4/7] adds genkey --- bit-wallet/bit | 2 ++ bit-wallet/bit-genkey | 20 ++++++++++++++++++++ lib/client/api.js | 41 +++++++++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100755 bit-wallet/bit-genkey diff --git a/bit-wallet/bit b/bit-wallet/bit index d1069d5..fbd840b 100755 --- a/bit-wallet/bit +++ b/bit-wallet/bit @@ -19,6 +19,8 @@ program .command('import', 'import wallet critical data') .command('confirm', 'show copayer\'s data for confirmation') .command('recreate', 'recreate a wallet on a remove server given local infomation') + .command('txproposals', 'list transactions proposals') + .command('genkey', 'generates extended private key for later wallet usage') .parse(process.argv); diff --git a/bit-wallet/bit-genkey b/bit-wallet/bit-genkey new file mode 100755 index 0000000..83b7164 --- /dev/null +++ b/bit-wallet/bit-genkey @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +var _ = require('lodash'); +var program = require('commander'); +var Client = require('../lib/client'); +var utils = require('./cli-utils'); +program = utils.configureCommander(program); + +program + .option('-t, --testnet', 'Create a Testnet Extended Private Key') + .parse(process.argv); + +var args = program.args; +var client = utils.getClient(program); +var network = program.testnet ? 'testnet' : 'livenet'; + +client.generateKey(network, function(err) { + utils.die(err); + console.log(' * ' + _.capitalize(network) + ' Extended Private Key Created.'); +}); diff --git a/lib/client/api.js b/lib/client/api.js index cec3b83..63d84cb 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -228,21 +228,27 @@ API.prototype._initData = function(network, walletPrivKey, m, n) { var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); var roPrivKey = xPrivKey.derive('m/1/0').privateKey; var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; - var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); var copayerId = WalletUtils.xPubToCopayerId(xPubKey); + var data = { copayerId: copayerId, xPrivKey: xPrivKey.toString(), publicKeyRing: [xPubKey], network: network, - m: m, - n: n, roPrivKey: roPrivKey.toWIF(), rwPrivKey: rwPrivKey.toWIF(), - walletPrivKey: walletPrivKey.toWIF(), - sharedEncryptingKey: sharedEncryptingKey, }; + + if (walletPrivKey) { + var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); + data.walletPrivKey = walletPrivKey.toWIF(); + data.sharedEncryptingKey = sharedEncryptingKey; + } + + if (m) data.m = m; + if (n) data.n = n; + return data; }; @@ -260,6 +266,22 @@ API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayer }); }; +API.prototype.generateKey = function(network, cb) { + var self = this; + network = network || 'livenet'; + if (!_.contains(['testnet', 'livenet'], network)) + return cb('Invalid network'); + + this.storage.load(function(err, data) { + if (data) + return cb(self.storage.getName() + ' already contains a wallet'); + + var data = self._initData(network); + self.storage.save(data, function(err) { + return cb(err, null); + }); + }); +}; API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) { var self = this; @@ -471,7 +493,7 @@ API.prototype.export = function(opts, cb) { opts = opts || {}; var access = opts.access || 'full'; - this._loadAndCheck({}, function(err, data) { + this._load(function(err, data) { if (err) return cb(err); var v = []; @@ -532,9 +554,12 @@ API.prototype.import = function(str, cb) { data.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); } - data.n = data.publicKeyRing.length; + var dataIsComplete = !!data.m; - if (!data.copayerId || !data.n || !data.m) + if (dataIsComplete) + data.n = data.publicKeyRing.length; + + if (!data.publicKeyRing) return cb('Invalid source data'); data.network = data.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; From e9010b5df6941d68f08a48324febc44cefcbb6a8 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 23 Feb 2015 14:09:04 -0300 Subject: [PATCH 5/7] airgapped working! --- lib/client/api.js | 348 +++++++++++++++++----------------- lib/clienterror.js | 4 + lib/expressapp.js | 4 +- test/integration/clientApi.js | 107 +++++------ 4 files changed, 239 insertions(+), 224 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 63d84cb..db61edb 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -11,11 +11,12 @@ log.debug = log.verbose; var Bitcore = require('bitcore') var WalletUtils = require('../walletutils'); var Verifier = require('./verifier'); -var ServerCompromisedError = require('./servercompromisederror') +var ServerCompromisedError = require('./servercompromisederror'); +var ClientError = require('../clienterror'); var BASE_URL = 'http://localhost:3001/copay/api'; -var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'publicKeyRing', 'sharedEncryptingKey']; +var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'n', 'publicKeyRing', 'sharedEncryptingKey']; var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; var WALLET_AIRGAPPED_TOCOMPLETE = ['publicKeyRing', 'm', 'n', 'sharedEncryptingKey']; @@ -54,13 +55,17 @@ function _parseError(body) { }; } } - var code = body.code || 'ERROR'; - var message = body.error || 'There was an unknown error processing the request'; - log.error(code, message); - return { - message: message, - code: code - }; + var ret; + if (body.code) { + ret = new ClientError(body.code, body.message); + } else { + ret = { + code: 'ERROR', + error: body.error || 'There was an unknown error processing the request', + }; + } + log.error(ret); + return ret; }; function _signRequest(method, url, args, privKey) { @@ -68,6 +73,34 @@ function _signRequest(method, url, args, privKey) { return WalletUtils.signMessage(message, privKey); }; +function _initWcd(network) { + $.checkArgument(network); + var xPrivKey = new Bitcore.HDPrivateKey(network); + var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); + var roPrivKey = xPrivKey.derive('m/1/0').privateKey; + var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; + var copayerId = WalletUtils.xPubToCopayerId(xPubKey); + + + return { + copayerId: copayerId, + xPrivKey: xPrivKey.toString(), + publicKeyRing: [xPubKey], + network: network, + roPrivKey: roPrivKey.toWIF(), + rwPrivKey: rwPrivKey.toWIF(), + }; +}; + +function _addWalletToWcd(wcd, walletPrivKey, m, n) { + $.checkArgument(wcd); + var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); + + wcd.walletPrivKey = walletPrivKey.toWIF(); + wcd.sharedEncryptingKey = sharedEncryptingKey; + wcd.m = m; + wcd.n = n; +}; function API(opts) { if (!opts.storage) { @@ -85,57 +118,60 @@ function API(opts) { } }; -API.prototype._tryToCompleteFromServer = function(data, cb) { - var self = this; +API.prototype._tryToCompleteFromServer = function(wcd, cb) { + if (!wcd.walletPrivKey) + return cb('Could not perform that action. Wallet Incomplete'); + + var self = this; var url = '/v1/wallets/'; - self._doGetRequest(url, data, function(err, ret) { + self._doGetRequest(url, wcd, function(err, ret) { if (err) return cb(err); var wallet = ret.wallet; if (wallet.status != 'complete') return cb('Wallet Incomplete'); - if (!Verifier.checkCopayers(wallet.copayers, data.walletPrivKey, - data.xPrivKey, data.n)) { + if (!Verifier.checkCopayers(wallet.copayers, wcd.walletPrivKey, + wcd.xPrivKey, wcd.n)) { return cb(new ServerCompromisedError( 'Copayers in the wallet could not be verified to have known the wallet secret')); } - data.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') + wcd.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') - self.storage.save(data, function(err) { - return cb(err, data); + self.storage.save(wcd, function(err) { + return cb(err, wcd); }); }); }; -API.prototype._tryToCompleteFromData = function(data, toComplete, cb) { +API.prototype._tryToCompleteFromData = function(wcd, toComplete, cb) { var inData = _decryptMessage(toComplete, - WalletUtils.privateKeyToAESKey(data.roPrivKey)); + WalletUtils.privateKeyToAESKey(wcd.roPrivKey)); if (!inData) return cb('Could not complete wallet'); try { inData = JSON.parse(inData); - _.extend(data, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); + _.extend(wcd, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); } catch (ex) { return cb(ex); } - this.storage.save(data, function(err) { - return cb(err, data); + this.storage.save(wcd, function(err) { + return cb(err, wcd); }); }; -API.prototype._tryToComplete = function(opts, data, cb) { +API.prototype._tryToComplete = function(opts, wcd, cb) { if (opts.toComplete) { - this._tryToCompleteFromData(data, opts.toComplete, cb); + this._tryToCompleteFromData(wcd, opts.toComplete, cb); } else { - this._tryToCompleteFromServer(data, cb); + this._tryToCompleteFromServer(wcd, cb); } }; @@ -144,11 +180,11 @@ API.prototype._tryToComplete = function(opts, data, cb) { API.prototype._load = function(cb) { var self = this; - this.storage.load(function(err, data) { - if (err || !data) { - return cb(err || 'Wallet file not found.'); + this.storage.load(function(err, wcd) { + if (err || !wcd) { + return cb(err || 'wcd file not found.'); } - return cb(null, data); + return cb(null, wcd); }); }; @@ -161,26 +197,27 @@ API.prototype._load = function(cb) { API.prototype._loadAndCheck = function(opts, cb) { var self = this; - this._load(function(err, data) { + this._load(function(err, wcd) { if (err) return cb(err); - if (!data.n || (data.n > 1 && data.publicKeyRing.length != data.n)) - return self._tryToComplete(opts, data, cb); + if (!wcd.n || (wcd.n > 1 && wcd.publicKeyRing.length != wcd.n)) { + return self._tryToComplete(opts, wcd, cb); + } - return cb(null, data); + return cb(null, wcd); }); }; -API.prototype._doRequest = function(method, url, args, data, cb) { +API.prototype._doRequest = function(method, url, args, wcd, cb) { var reqSignature; - data = data || {}; + wcd = wcd || {}; if (method == 'get') { - if (data.roPrivKey) - reqSignature = _signRequest(method, url, args, data.roPrivKey); + if (wcd.roPrivKey) + reqSignature = _signRequest(method, url, args, wcd.roPrivKey); } else { - if (data.rwPrivKey) - reqSignature = _signRequest(method, url, args, data.rwPrivKey); + if (wcd.rwPrivKey) + reqSignature = _signRequest(method, url, args, wcd.rwPrivKey); } var absUrl = this.baseUrl + url; @@ -188,7 +225,7 @@ API.prototype._doRequest = function(method, url, args, data, cb) { // relUrl: only for testing with `supertest` relUrl: this.basePath + url, headers: { - 'x-identity': data.copayerId, + 'x-identity': wcd.copayerId, 'x-signature': reqSignature, }, method: method, @@ -214,44 +251,15 @@ API.prototype._doRequest = function(method, url, args, data, cb) { }; -API.prototype._doPostRequest = function(url, args, data, cb) { - return this._doRequest('post', url, args, data, cb); +API.prototype._doPostRequest = function(url, args, wcd, cb) { + return this._doRequest('post', url, args, wcd, cb); }; -API.prototype._doGetRequest = function(url, data, cb) { - return this._doRequest('get', url, {}, data, cb); +API.prototype._doGetRequest = function(url, wcd, cb) { + return this._doRequest('get', url, {}, wcd, cb); }; -API.prototype._initData = function(network, walletPrivKey, m, n) { - var xPrivKey = new Bitcore.HDPrivateKey(network); - var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); - var roPrivKey = xPrivKey.derive('m/1/0').privateKey; - var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; - var copayerId = WalletUtils.xPubToCopayerId(xPubKey); - - - var data = { - copayerId: copayerId, - xPrivKey: xPrivKey.toString(), - publicKeyRing: [xPubKey], - network: network, - roPrivKey: roPrivKey.toWIF(), - rwPrivKey: rwPrivKey.toWIF(), - }; - - if (walletPrivKey) { - var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); - data.walletPrivKey = walletPrivKey.toWIF(); - data.sharedEncryptingKey = sharedEncryptingKey; - } - - if (m) data.m = m; - if (n) data.n = n; - - return data; -}; - API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) { var args = { walletId: walletId, @@ -272,12 +280,12 @@ API.prototype.generateKey = function(network, cb) { if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); - this.storage.load(function(err, data) { - if (data) + this.storage.load(function(err, wcd) { + if (wcd) return cb(self.storage.getName() + ' already contains a wallet'); - var data = self._initData(network); - self.storage.save(data, function(err) { + var wcd = _initWcd(network); + self.storage.save(wcd, function(err) { return cb(err, null); }); }); @@ -289,10 +297,13 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); - this.storage.load(function(err, data) { - if (data) + this.storage.load(function(err, wcd) { + if (wcd && wcd.n) return cb(self.storage.getName() + ' already contains a wallet'); + if (wcd && wcd.network && wcd.network != network) + return cb('Storage ' + self.storage.getName() + ' is set to network:' + wcd.network); + var walletPrivKey = new Bitcore.PrivateKey(); var args = { name: walletName, @@ -308,11 +319,14 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb var walletId = body.walletId; var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); - var data = self._initData(network, walletPrivKey, m, n); - self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[0], copayerName, + + wcd = wcd || _initWcd(network); + _addWalletToWcd(wcd, walletPrivKey, m, n) + + self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[0], copayerName, function(err, wallet) { if (err) return cb(err); - self.storage.save(data, function(err) { + self.storage.save(wcd, function(err) { return cb(err, n > 1 ? secret : null); }); }); @@ -323,16 +337,16 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb API.prototype.reCreateWallet = function(walletName, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var walletPrivKey = new Bitcore.PrivateKey(); var args = { name: walletName, - m: data.m, - n: data.n, + m: wcd.m, + n: wcd.n, pubKey: walletPrivKey.toPublicKey().toString(), - network: data.network, + network: wcd.network, }; var url = '/v1/wallets/'; self._doPostRequest(url, args, {}, function(err, body) { @@ -340,11 +354,11 @@ API.prototype.reCreateWallet = function(walletName, cb) { var walletId = body.walletId; - var secret = WalletUtils.toSecret(walletId, walletPrivKey, data.network); + var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network); var i = 0; - async.each(data.publicKeyRing, function(xpub, next) { + async.each(wcd.publicKeyRing, function(xpub, next) { var copayerName = 'recovered Copayer #' + i; - self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[i++], copayerName, next); + self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next); }, function(err) { return cb(err); }); @@ -356,22 +370,22 @@ API.prototype.reCreateWallet = function(walletName, cb) { API.prototype.joinWallet = function(secret, copayerName, cb) { var self = this; - this.storage.load(function(err, data) { - if (data) - return cb('Storage already contains a wallet'); + this.storage.load(function(err, wcd) { + if (wcd && wcd.n) + return cb(self.storage.getName() + ' already contains a wallet'); try { var secretData = WalletUtils.fromSecret(secret); } catch (ex) { return cb(ex); } - var data = self._initData(secretData.network, secretData.walletPrivKey); - self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, data.publicKeyRing[0], copayerName, - function(err, wallet) { + wcd = wcd || _initWcd(secretData.network); + + self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, wcd.publicKeyRing[0], copayerName, + function(err, joinedWallet) { if (err) return cb(err); - data.m = wallet.m; - data.n = wallet.n; - self.storage.save(data, cb); + _addWalletToWcd(wcd, secretData.walletPrivKey, joinedWallet.m, joinedWallet.n); + self.storage.save(wcd, cb); }); }); }; @@ -379,13 +393,13 @@ API.prototype.joinWallet = function(secret, copayerName, cb) { API.prototype.getStatus = function(cb) { var self = this; - this._load(function(err, data) { + this._load(function(err, wcd) { if (err) return cb(err); var url = '/v1/wallets/'; - self._doGetRequest(url, data, function(err, result) { - _processTxps(result.pendingTxps, data.sharedEncryptingKey); - return cb(err, result, data.copayerId); + self._doGetRequest(url, wcd, function(err, result) { + _processTxps(result.pendingTxps, wcd.sharedEncryptingKey); + return cb(err, result, wcd.copayerId); }); }); }; @@ -404,36 +418,36 @@ API.prototype.sendTxProposal = function(opts, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - if (!data.rwPrivKey) + if (!wcd.rwPrivKey) return cb('No key to generate proposals'); var args = { toAddress: opts.toAddress, amount: opts.amount, - message: _encryptMessage(opts.message, data.sharedEncryptingKey), + message: _encryptMessage(opts.message, wcd.sharedEncryptingKey), }; var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); - args.proposalSignature = WalletUtils.signMessage(hash, data.rwPrivKey); + args.proposalSignature = WalletUtils.signMessage(hash, wcd.rwPrivKey); log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); var url = '/v1/txproposals/'; - self._doPostRequest(url, args, data, cb); + self._doPostRequest(url, args, wcd, cb); }); }; API.prototype.createAddress = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/addresses/'; - self._doPostRequest(url, {}, data, function(err, address) { + self._doPostRequest(url, {}, wcd, function(err, address) { if (err) return cb(err); - if (!Verifier.checkAddress(data, address)) { + if (!Verifier.checkAddress(wcd, address)) { return cb(new ServerCompromisedError('Server sent fake address')); } @@ -449,16 +463,16 @@ API.prototype.createAddress = function(cb) { API.prototype.getMainAddresses = function(opts, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/addresses/'; - self._doGetRequest(url, data, function(err, addresses) { + self._doGetRequest(url, wcd, function(err, addresses) { if (err) return cb(err); if (!opts.doNotVerify) { var fake = _.any(addresses, function(address) { - return !Verifier.checkAddress(data, address); + return !Verifier.checkAddress(wcd, address); }); if (fake) return cb(new ServerCompromisedError('Server sent fake address')); @@ -475,15 +489,16 @@ API.prototype.history = function(limit, cb) { API.prototype.getBalance = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/balance/'; - self._doGetRequest(url, data, cb); + self._doGetRequest(url, wcd, cb); }); }; /** - * export + * Export does not try to complete the wallet from the server. Exports the + * wallet as it is now. * * @param opts.access =['full', 'readonly', 'readwrite'] */ @@ -493,11 +508,11 @@ API.prototype.export = function(opts, cb) { opts = opts || {}; var access = opts.access || 'full'; - this._load(function(err, data) { + this._load(function(err, wcd) { if (err) return cb(err); var v = []; - var myXPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString(); + var myXPubKey = (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString(); _.each(WALLET_CRITICAL_DATA, function(k) { var d; @@ -509,18 +524,18 @@ API.prototype.export = function(opts, cb) { // Skips own pub key IF priv key is exported if (access == 'full' && k === 'publicKeyRing') { - d = _.without(data[k], myXPubKey); + d = _.without(wcd[k], myXPubKey); } else { - d = data[k]; + d = wcd[k]; } v.push(d); }); if (access != 'full') { - v.push(data.copayerId); - v.push(data.roPrivKey); + v.push(wcd.copayerId); + v.push(wcd.roPrivKey); if (access == 'readwrite') { - v.push(data.rwPrivKey); + v.push(wcd.rwPrivKey); } } @@ -532,39 +547,34 @@ API.prototype.export = function(opts, cb) { API.prototype.import = function(str, cb) { var self = this; - this.storage.load(function(err, data) { - if (data) + this.storage.load(function(err, wcd) { + if (wcd) return cb('Storage already contains a wallet'); - data = {}; + wcd = {}; var inData = JSON.parse(str); var i = 0; _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { - data[k] = inData[i++]; + wcd[k] = inData[i++]; }); - if (data.xPrivKey) { - var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey); + if (wcd.xPrivKey) { + var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey); var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); - data.publicKeyRing.unshift(xPubKey); - data.copayerId = WalletUtils.xPubToCopayerId(xPubKey); - data.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); - data.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); + wcd.publicKeyRing.unshift(xPubKey); + wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey); + wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); + wcd.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); } - var dataIsComplete = !!data.m; + if (!wcd.publicKeyRing) + return cb('Invalid source wallet'); - if (dataIsComplete) - data.n = data.publicKeyRing.length; - - if (!data.publicKeyRing) - return cb('Invalid source data'); - - data.network = data.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; - self.storage.save(data, function(err) { - return cb(err, WalletUtils.accessFromData(data)); + wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; + self.storage.save(wcd, function(err) { + return cb(err, WalletUtils.accessFromData(wcd)); }); }); }; @@ -578,14 +588,14 @@ API.prototype.parseTxProposals = function(txData, cb) { this._loadAndCheck({ toComplete: txData.toComplete - }, function(err, data) { + }, function(err, wcd) { if (err) return cb(err); var txps = txData.txps; - _processTxps(txps, data.sharedEncryptingKey); + _processTxps(txps, wcd.sharedEncryptingKey); var fake = _.any(txps, function(txp) { - return (!Verifier.checkTxProposal(data, txp)); + return (!Verifier.checkTxProposal(wcd, txp)); }); if (fake) @@ -607,20 +617,20 @@ API.prototype.parseTxProposals = function(txData, cb) { API.prototype.getTxProposals = function(opts, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/'; - self._doGetRequest(url, data, function(err, txps) { + self._doGetRequest(url, wcd, function(err, txps) { if (err) return cb(err); var rawTxps; if (opts.getRawTxps) rawTxps = JSON.parse(JSON.stringify(txps)); - _processTxps(txps, data.sharedEncryptingKey); + _processTxps(txps, wcd.sharedEncryptingKey); var fake = _.any(txps, function(txp) { - return (!opts.doNotVerify && !Verifier.checkTxProposal(data, txp)); + return (!opts.doNotVerify && !Verifier.checkTxProposal(wcd, txp)); }); if (fake) @@ -631,14 +641,14 @@ API.prototype.getTxProposals = function(opts, cb) { }); }; -API.prototype._getSignaturesFor = function(txp, data) { +API.prototype._getSignaturesFor = function(txp, wcd) { //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 xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey, network); _.each(txp.inputs, function(i) { if (!derived[i.path]) { @@ -671,24 +681,24 @@ API.prototype.getSignatures = function(txp, cb) { $.checkArgument(txp.creatorId); var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - if (!Verifier.checkTxProposal(data, txp)) { + if (!Verifier.checkTxProposal(wcd, txp)) { return cb(new ServerCompromisedError('Transaction proposal is invalid')); } - return cb(null, self._getSignaturesFor(txp, data)); + return cb(null, self._getSignaturesFor(txp, wcd)); }); }; API.prototype.getEncryptedWalletData = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - var toComplete = JSON.stringify(_.pick(data, WALLET_AIRGAPPED_TOCOMPLETE)); - return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(data.roPrivKey))); + var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE)); + return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey))); }); }; @@ -699,21 +709,21 @@ API.prototype.signTxProposal = function(txp, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - if (!Verifier.checkTxProposal(data, txp)) { + if (!Verifier.checkTxProposal(wcd, txp)) { return cb(new ServerCompromisedError('Server sent fake transaction proposal')); } - var signatures = txp.signatures || self._getSignaturesFor(txp, data); + var signatures = txp.signatures || self._getSignaturesFor(txp, wcd); var url = '/v1/txproposals/' + txp.id + '/signatures/'; var args = { signatures: signatures }; - self._doPostRequest(url, args, data, cb); + self._doPostRequest(url, args, wcd, cb); }); }; @@ -723,14 +733,14 @@ API.prototype.rejectTxProposal = function(txp, reason, cb) { var self = this; this._loadAndCheck({}, - function(err, data) { + function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/' + txp.id + '/rejections/'; var args = { - reason: _encryptMessage(reason, data.sharedEncryptingKey) || '', + reason: _encryptMessage(reason, wcd.sharedEncryptingKey) || '', }; - self._doPostRequest(url, args, data, cb); + self._doPostRequest(url, args, wcd, cb); }); }; @@ -738,11 +748,11 @@ API.prototype.broadcastTxProposal = function(txp, cb) { var self = this; this._loadAndCheck({}, - function(err, data) { + function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - self._doPostRequest(url, {}, data, cb); + self._doPostRequest(url, {}, wcd, cb); }); }; @@ -751,10 +761,10 @@ API.prototype.broadcastTxProposal = function(txp, cb) { API.prototype.removeTxProposal = function(txp, cb) { var self = this; this._loadAndCheck({}, - function(err, data) { + function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/' + txp.id; - self._doRequest('delete', url, {}, data, cb); + self._doRequest('delete', url, {}, wcd, cb); }); }; diff --git a/lib/clienterror.js b/lib/clienterror.js index 2e6fd84..f0f4015 100644 --- a/lib/clienterror.js +++ b/lib/clienterror.js @@ -18,4 +18,8 @@ function ClientError() { } }; +ClientError.prototype.toString = function() { + return ''; +}; + module.exports = ClientError; diff --git a/lib/expressapp.js b/lib/expressapp.js index e253af5..083ab5b 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -64,11 +64,11 @@ ExpressApp.start = function(opts) { var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400; if (!opts.disableLogs) - log.error('Err: ' + status + ':' + req.url + ' :' + err.code + ':' + err.message); + log.info('Client Err: ' + status + ' ' + req.url + ' ' + err); res.status(status).json({ code: err.code, - error: err.message, + message: err.message, }).end(); } else { var code, message; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 6d09aef..d9d828a 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -377,6 +377,8 @@ describe('client API ', function() { should.not.exist(err); clients[1].import(str, function(err, wallet) { should.not.exist(err); + + console.log('[clientApi.js.380]'); //TODO clients[1].createAddress(function(err, x0) { err.code.should.equal('NOTAUTHORIZED'); clients[0].createAddress(function(err, x0) { @@ -439,7 +441,7 @@ describe('client API ', function() { }); }); }); - describe('Air gapped flows', function() { + describe('Air gapped related 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); @@ -505,66 +507,65 @@ describe('client API ', function() { }); }); - it('should complete public key ring from file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { + it('should create from proxy from airgapped', function(done) { + // client0 -> airgapped + // client1 -> proxy + clients[0].generateKey('testnet', function(err) { should.not.exist(err); - - clients[1].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - var opts = { - amount: 10000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[1].sendTxProposal(opts, function(err, x) { + clients[0].export({ + access: 'readwrite' + }, function(err, str) { + clients[1].import(str, function(err) { should.not.exist(err); - // Create the proxy, ro, connected, device (2) - clients[0].export({ - access: 'readonly' - }, function(err, str) { - should.not.exist(err); - clients[2].import(str, function(err, wallet) { + clients[1].createWallet('1', '2', 1, 1, 'testnet', + function(err) { should.not.exist(err); - - clients[2].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - - clients[2].getEncryptedWalletData(function(err, toComplete) { - should.not.exist(err); - - // Disable networking - clients[0].request = sinon.stub().yields('no network'); - - // Make client incomplete - var data = JSON.parse(fsmock._get('client0')); - delete data.n; - fsmock._set('client0', JSON.stringify(data)); - - // Back to the air gapped - // - // Will trigger _tryToComplete and use pkr - // then, needs pkr to verify the txps - - clients[0].parseTxProposals({ - txps: rawTxps, - toComplete: toComplete, - }, function(err, txs2) { - should.not.exist(err); - done(); - }); - }); + // should keep cpub + var c0 = JSON.parse(fsmock._get('client0')); + var c1 = JSON.parse(fsmock._get('client1')); + _.each(['copayerId', 'network', 'publicKeyRing', + 'roPrivKey', 'rwPrivKey' + ], function(k) { + c0[k].should.deep.equal(c1[k]); }); + done(); }); + }); + }); + }); + }); + + it('should join from proxy from airgapped', function(done) { + // client0 -> airgapped + // client1 -> proxy + clients[0].generateKey('testnet', function(err) { + should.not.exist(err); + clients[0].export({ + access: 'readwrite' + }, function(err, str) { + clients[1].import(str, function(err) { + should.not.exist(err); + clients[2].createWallet('1', '2', 1, 2, 'testnet', function(err, secret) { + should.not.exist(err); + clients[1].joinWallet(secret, 'john', function(err) { + should.not.exist(err); + // should keep cpub + var c0 = JSON.parse(fsmock._get('client0')); + var c1 = JSON.parse(fsmock._get('client1')); + _.each(['copayerId', 'network', 'publicKeyRing', + 'roPrivKey', 'rwPrivKey' + ], function(k) { + c0[k].should.deep.equal(c1[k]); + }); + done(); + }) }); }); }); }); }); + it('should be able export signatures and sign later from a ro client', function(done) { helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { @@ -704,9 +705,9 @@ describe('client API ', function() { it('round trip #import #export', function(done) { helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { should.not.exist(err); - clients[0].export({}, function(err, str) { + clients[1].export({}, function(err, str) { should.not.exist(err); - var original = JSON.parse(fsmock._get('client0')); + var original = JSON.parse(fsmock._get('client1')); clients[2].import(str, function(err, wallet) { should.not.exist(err); var clone = JSON.parse(fsmock._get('client2')); @@ -719,7 +720,7 @@ describe('client API ', function() { }); }); it('should recreate a wallet, create addresses and receive money', function(done) { - var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; + var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; clients[0].import(backup, function(err, wallet) { should.not.exist(err); clients[0].reCreateWallet('pepe', function(err, wallet) { From e3482e01b81d986d7c67056009b0055083b7f92e Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 23 Feb 2015 14:19:52 -0300 Subject: [PATCH 6/7] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb00b87..ad055ad 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ bit import output.dat bit recreate ``` -# Airgapped Operation (TODO) +# Airgapped Operation ### On the Air-gapped device ``` From 74d0908aec95df3047eebe21a3be98adfa997414 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 23 Feb 2015 15:36:35 -0300 Subject: [PATCH 7/7] better tests --- README.md | 26 +++++++++++++------------- test/integration/clientApi.js | 33 +++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ad055ad..ca0441b 100644 --- a/README.md +++ b/README.md @@ -95,53 +95,53 @@ bit recreate ### On the Air-gapped device ``` -git genkey -git export -o wallet.dat --readonly (or --nosigning) +bit genkey +bit export -o wallet.dat --readonly (or --nosigning) ``` ### Proxy machine ``` -git join secret -i wallet.dat -git balance +bit join secret -i wallet.dat +bit balance # Export pending transaction to be signed offline -git txproposals -o txproposals.dat +bit txproposals -o txproposals.dat ``` ## Back to air-gapped device ### To check tx proposals: ``` -git txproposals -i txproposals.dat +bit txproposals -i txproposals.dat ``` First time txproposals is running on the air gapped devices, the public keys of the copayers will be imported from the txproposals archive. That information is exported automatically by the proxy machine, and encrypted copayer's xpriv derivatives. ### Sign them ``` -git sign -i txproposals.dat -o txproposals-signed.dat +bit sign -i txproposals.dat -o txproposals-signed.dat # Or With filter -git sign e01e -i txproposals.dat -o txproposals-signed.dat +bit sign e01e -i txproposals.dat -o txproposals-signed.dat ``` ## Back to proxy machine ``` -git sign -i txproposals-signed.dat +bit sign -i txproposals-signed.dat ``` # Password protection (TODO) ### encrypts everything by default ``` -git create myWallet 2-3 -p password +bit create myWallet 2-3 -p password # Or (interactive mode) -git create myWallet 2-3 -p +bit create myWallet 2-3 -p Enter password: ``` ### allows readonly operations without password (encrypts xpriv, and leave readonlySigningKey unencrypted) ``` -git create myWallet 2-3 -p --nopasswd:ro +bit create myWallet 2-3 -p --nopasswd:ro ``` ### allows readwrite operations without password (only encrypts xpriv) ``` -git create myWallet 2-3 -p --nopasswd:rw +bit create myWallet 2-3 -p --nopasswd:rw ``` # Local data diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index d9d828a..ae4413b 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -378,7 +378,6 @@ describe('client API ', function() { clients[1].import(str, function(err, wallet) { should.not.exist(err); - console.log('[clientApi.js.380]'); //TODO clients[1].createAddress(function(err, x0) { err.code.should.equal('NOTAUTHORIZED'); clients[0].createAddress(function(err, x0) { @@ -508,17 +507,19 @@ describe('client API ', function() { }); it('should create from proxy from airgapped', function(done) { - // client0 -> airgapped - // client1 -> proxy - clients[0].generateKey('testnet', function(err) { + + var airgapped = clients[0]; + var proxy = clients[1]; + + airgapped.generateKey('testnet', function(err) { should.not.exist(err); - clients[0].export({ + airgapped.export({ access: 'readwrite' }, function(err, str) { - clients[1].import(str, function(err) { + proxy.import(str, function(err) { should.not.exist(err); - clients[1].createWallet('1', '2', 1, 1, 'testnet', + proxy.createWallet('1', '2', 1, 1, 'testnet', function(err) { should.not.exist(err); // should keep cpub @@ -537,18 +538,22 @@ describe('client API ', function() { }); it('should join from proxy from airgapped', function(done) { - // client0 -> airgapped - // client1 -> proxy - clients[0].generateKey('testnet', function(err) { + + var airgapped = clients[0]; + var proxy = clients[1]; + var other = clients[2]; // Other copayer + + airgapped.generateKey('testnet', function(err) { should.not.exist(err); - clients[0].export({ + airgapped.export({ access: 'readwrite' }, function(err, str) { - clients[1].import(str, function(err) { + proxy.import(str, function(err) { should.not.exist(err); - clients[2].createWallet('1', '2', 1, 2, 'testnet', function(err, secret) { + + other.createWallet('1', '2', 1, 2, 'testnet', function(err, secret) { should.not.exist(err); - clients[1].joinWallet(secret, 'john', function(err) { + proxy.joinWallet(secret, 'john', function(err) { should.not.exist(err); // should keep cpub var c0 = JSON.parse(fsmock._get('client0'));