From e12b6b815842aba0cc147949b656c02882836507 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 21 Feb 2015 22:53:00 -0300 Subject: [PATCH] add access levels to import / export --- bit-wallet/bit-export | 48 +++++++++++++++++----- bit-wallet/bit-import | 2 +- lib/client/api.js | 55 +++++++++++++++++-------- lib/walletutils.js | 10 +++++ test/integration/clientApi.js | 75 +++++++++++++++++++++++++++++++++-- 5 files changed, 161 insertions(+), 29 deletions(-) diff --git a/bit-wallet/bit-export b/bit-wallet/bit-export index 3e876c8..3b16d47 100755 --- a/bit-wallet/bit-export +++ b/bit-wallet/bit-export @@ -1,27 +1,57 @@ #!/usr/bin/env node var program = require('commander'); -var qr = require('qr-image'); +var qr = require('qr-image'); +var fs = require('fs'); +var _ = require('lodash'); var Client = require('../lib/client'); -var utils = require('./cli-utils'); +var utils = require('./cli-utils'); program = utils.configureCommander(program); program - .option('-q, --qr') + .option('-a, --access [level]', 'access privileges for exported data (full, readwrite, readonly)', 'full') + .option('-q, --qr', 'export a QR code') + .option('-o, --output [filename]', 'output file'); + +program.on('--help', function() { + console.log(' Access Levels:'); + console.log(''); + console.log(' readonly : allows to read wallet data: balance, tx proposals '); + console.log(' readwrite: + allows to create addresses and unsigned tx prposals '); + console.log(' full : + allows sign tx prposals '); + console.log(''); +}); + +program .parse(process.argv); var args = program.args; var client = utils.getClient(program); -client.export(function(err, x) { +if (!_.contains(['full', 'readwrite', 'readonly'], program.access)) { + program.help(); +} + +var msg = ' Access Level: ' + program.access; + +client.export({ + access: program.access +}, function(err, x) { utils.die(err); if (program.qr) { - var filename = program.config + '.svg'; - var qr_svg = qr.image(x, { type: 'svg' }); - qr_svg.pipe(require('fs').createWriteStream(filename)); - console.log('Wallet Critical Data: exported to ' + filename); + var filename = program.file + '.svg'; + var qr_svg = qr.image(x, { + type: 'svg' + }); + qr_svg.pipe(fs.createWriteStream(filename)); + console.log('Wallet Critical Data: exported to %s. %s\n',filename, msg); } else { - console.log('Wallet Critical Data:\n', x); + if (program.output) { + fs.writeFileSync(program.output, x); + console.log('Wallet Critical Data saved at %s. %s\n', program.output, msg); + } else { + console.log('Wallet Critical Data (%s)\n%s', msg, x); + } } }); diff --git a/bit-wallet/bit-import b/bit-wallet/bit-import index 0b5f4f8..1a2d7df 100755 --- a/bit-wallet/bit-import +++ b/bit-wallet/bit-import @@ -22,5 +22,5 @@ var str = fs.readFileSync(args[0]); client.import(str, function(err, x) { utils.die(err); - console.log('Wallet Imported'); + console.log('Wallet Imported. Access level:' + x); }); diff --git a/lib/client/api.js b/lib/client/api.js index 8181de7..1caa8a8 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -16,6 +16,7 @@ var ServerCompromisedError = require('./servercompromisederror') var BASE_URL = 'http://localhost:3001/copay/api'; var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'publicKeyRing', 'sharedEncryptingKey']; +var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; function _encryptMessage(message, encryptingKey) { if (!message) return null; @@ -398,8 +399,16 @@ API.prototype.getBalance = function(cb) { }); }; -API.prototype.export = function(cb) { +/** + * export + * + * @param opts.access =['full', 'readonly', 'readwrite'] + */ +API.prototype.export = function(opts, cb) { var self = this; + $.shouldBeFunction(cb); + opts = opts || {}; + var access = opts.access || 'full'; this._loadAndCheck(function(err, data) { if (err) return cb(err); @@ -409,13 +418,29 @@ API.prototype.export = function(cb) { _.each(WALLET_CRITICAL_DATA, function(k) { var d; - if (k === 'publicKeyRing') { + + if (access != 'full' && k === 'xPrivKey') { + v.push(null); + return; + } + + // Skips own pub key IF priv key is exported + if (access == 'full' && k === 'publicKeyRing') { d = _.without(data[k], myXPubKey); } else { d = data[k]; } v.push(d); }); + + if (access != 'full') { + v.push(data.copayerId); + v.push(data.roPrivKey); + if (access == 'readwrite') { + v.push(data.rwPrivKey); + } + } + return cb(null, JSON.stringify(v)); }); } @@ -433,30 +458,28 @@ API.prototype.import = function(str, cb) { var inData = JSON.parse(str); var i = 0; - _.each(WALLET_CRITICAL_DATA, function(k) { + _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { data[k] = inData[i++]; - if (!data[k]) - return cb('Invalid wallet data'); }); if (data.xPrivKey) { - var xPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString(); + var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey); + var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); data.publicKeyRing.unshift(xPubKey); data.copayerId = WalletUtils.xPubToCopayerId(xPubKey); - } else { - data.copayerId = inData[i]; + data.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); + data.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); } - if (!data.copayerId) - return cb('Invalid source data'); - data.n = data.publicKeyRing.length; - var xpriv = data.xPrivKey ? new Bitcore.HDPrivateKey(data.xPrivKey) : null; - data.roPrivKey = inData.roPrivKey || (xpriv ? xpriv.derive('m/1/0').privateKey.toWIF() : null); - data.rwPrivKey = inData.rwPrivKey || (xpriv ? xpriv.derive('m/1/1').privateKey.toWIF() : null); - data.network = data.xPrivKey.substr(0, 4) === 'tprv' ? 'testnet' : 'livenet'; - self.storage.save(data, cb); + if (!data.copayerId || !data.n || !data.m) + 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)); + }); }); }; diff --git a/lib/walletutils.js b/lib/walletutils.js index 7157fb9..a5d151c 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -31,6 +31,16 @@ WalletUtils.signMessage = function(text, privKey) { }; +WalletUtils.accessFromData = function(data) { + if (data.xPrivKey) + return 'full'; + + if (data.rwPrivKey) + return 'readwrite'; + + return 'readonly'; +}; + WalletUtils.verifyMessage = function(text, signature, pubKey) { $.checkArgument(text); $.checkArgument(pubKey); diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index e11ddd9..8d8c486 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -277,7 +277,7 @@ describe('client API ', function() { }); }); - describe('Access control', function() { + describe('Access control & export', 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); @@ -292,8 +292,77 @@ describe('client API ', function() { }); }); }); - }); + it('should not be able to create address from a ro export', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function(err) { + should.not.exist(err); + clients[0].export({ + access: 'readonly' + }, function(err, str) { + should.not.exist(err); + clients[1].import(str, function(err, wallet) { + should.not.exist(err); + clients[1].createAddress(function(err, x0) { + err.code.should.equal('NOTAUTHORIZED'); + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + }); + it('should be able to create address from a rw export', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function(err) { + should.not.exist(err); + clients[0].export({ + access: 'readwrite' + }, function(err, str) { + should.not.exist(err); + clients[1].import(str, function(err, wallet) { + should.not.exist(err); + clients[1].createAddress(function(err, x0) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + it('should not be able to create tx proposals from a rw export', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + should.not.exist(err); + clients[0].export({ + access: 'readwrite' + }, function(err, str) { + clients[1].import(str, function(err, wallet) { + 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) { + should.not.exist(err); + clients[1].signTxProposal(x, function(err, tx) { + err.code.should.be.equal('BADSIGNATURES'); + clients[1].getTxProposals({}, function(err, txs) { + should.not.exist(err); + txs[0].status.should.equal('pending'); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); describe('Address Creation', function() { it('should be able to create address in all copayers in a 2-3 wallet', function(done) { @@ -395,7 +464,7 @@ 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[0].export({}, function(err, str) { should.not.exist(err); var original = JSON.parse(fsmock._get('client0')); clients[2].import(str, function(err, wallet) {