diff --git a/bit-wallet/bit-create b/bit-wallet/bit-create index 9b484be..ddb4195 100755 --- a/bit-wallet/bit-create +++ b/bit-wallet/bit-create @@ -8,6 +8,7 @@ program = utils.configureCommander(program); program .option('-t, --testnet', 'Create a Testnet Wallet') + .option('-n, --nopasswd [level]', 'Set access for no password usage: none(default), readonly, readwrite, full', 'none') .usage('[options] [copayerName]') .parse(process.argv); diff --git a/bit-wallet/bit-genkey b/bit-wallet/bit-genkey index 83b7164..fe3102d 100755 --- a/bit-wallet/bit-genkey +++ b/bit-wallet/bit-genkey @@ -8,6 +8,7 @@ program = utils.configureCommander(program); program .option('-t, --testnet', 'Create a Testnet Extended Private Key') + .option('-n, --nopasswd [level]', 'Set access for no password usage: none(default), readonly, readwrite, full', 'none') .parse(process.argv); var args = program.args; diff --git a/bit-wallet/bit-join b/bit-wallet/bit-join index a4bc9a6..fb169b6 100755 --- a/bit-wallet/bit-join +++ b/bit-wallet/bit-join @@ -7,6 +7,7 @@ program = utils.configureCommander(program); program .usage('[options] [copayerName]') + .option('-n, --nopasswd [level]', 'Set access for no password usage: none(default), readonly, readwrite, full', 'none') .parse(process.argv); var args = program.args; diff --git a/bit-wallet/cli-utils.js b/bit-wallet/cli-utils.js index 4c63f05..6bbbfff 100644 --- a/bit-wallet/cli-utils.js +++ b/bit-wallet/cli-utils.js @@ -1,5 +1,6 @@ var _ = require('lodash'); var Client = require('../lib/client'); +var read = require('read') var Utils = function() {}; @@ -38,12 +39,27 @@ Utils.getClient = function(args) { var storage = new Client.FileStorage({ filename: args.file || process.env['BIT_FILE'], }); - return new Client({ + var c = new Client({ storage: storage, baseUrl: args.host || process.env['BIT_HOST'], - verbose: args.verbose - password: args.password, + verbose: args.verbose, }); + + + if (args.nopasswd) + c.setNopasswdAccess(args.nopasswd); + + c.on('needPassword', function(cb) { + if (args.password) { + return cb(args.password); + } else { + read({ prompt: 'Password: ', silent: true }, function(er, password) { + return cb(password); + }) + } + }); + + return c; } Utils.findOneTxProposal = function(txps, id) { diff --git a/lib/client/api.js b/lib/client/api.js index 17c02df..fc32536 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -6,6 +6,7 @@ var util = require('util'); var async = require('async'); var log = require('npmlog'); var request = require('request') +var events = require('events'); log.debug = log.verbose; var Bitcore = require('bitcore') @@ -111,6 +112,7 @@ function API(opts) { this.request = request || opts.request; this.baseUrl = opts.baseUrl || BASE_URL; this.basePath = this.baseUrl.replace(/http.?:\/\/[a-zA-Z0-9:-]*\//, '/'); + this.noPasswdAccess = opts.noPasswdAccess || 'full'; if (this.verbose) { log.level = 'debug'; } else { @@ -118,7 +120,7 @@ function API(opts) { } }; - +util.inherits(API, events.EventEmitter); API.prototype._tryToCompleteFromServer = function(wcd, cb) { @@ -143,7 +145,7 @@ API.prototype._tryToCompleteFromServer = function(wcd, cb) { wcd.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') - self.storage.save(wcd, function(err) { + self.save(wcd, function(err) { return cb(err, wcd); }); }); @@ -163,7 +165,7 @@ API.prototype._tryToCompleteFromData = function(wcd, toComplete, cb) { return cb(ex); } - this.storage.save(wcd, function(err) { + this.save(wcd, function(err) { return cb(err, wcd); }); }; @@ -202,8 +204,16 @@ API.prototype._processWcdAfterRead = function(rawData, requiredAccess, cb) { // Decrypt it and try again this.emit('needPassword', function(password) { if (!password) return cb('No password'); - rawData = WE.decryptWallet(rawData, password); - var access = WU.accessFromData(rawData); + + try { + rawData = WU.decryptWallet(rawData, password); + } catch (e) {}; + + if (!rawData) + return cb('NOTAUTH'); + + access = WU.accessFromData(rawData); + accessLevel = WU.accessNameToLevel(access); // Is the data available? if (requiredAccessLevel <= accessLevel) @@ -213,16 +223,25 @@ API.prototype._processWcdAfterRead = function(rawData, requiredAccess, cb) { }); }; -API.prototype._processWcdBeforeWrite = function(wcd, accessWithoutEncrytion, cb) { + +API.prototype.setNopasswdAccess = function(noPasswdAccess) { + if (!_.contains(['none', 'readonly', 'readwrite', 'full'], noPasswdAccess)) + throw new Error('Bad nopasswd access:' + noPasswdAccess); + + this.noPasswdAccess = noPasswdAccess; +}; + +API.prototype._processWcdBeforeWrite = function(wcd, cb) { + var self = this; // Is any encrypted? - if (encryptedAccess) { - this.emit('needPassword', function(password) { - if (!password) return cb('No password'); - rawdata = WE.encryptWallet(wcd, accessWithoutEncrytion, password); - return cb(null, rawdata); - }); + if (this.noPasswdAccess == 'full') { + return cb(null, wcd); } else { - return rawdata; + this.emit('needPassword', function(password) { + if (!password) return cb('No password given'); + var ewcd = WalletUtils.encryptWallet(wcd, self.noPasswdAccess, password); + return cb(null, ewcd); + }); } }; @@ -326,6 +345,18 @@ API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayer }); }; +API.prototype.save = function(inWcd, cb) { + var self = this; + + self._processWcdBeforeWrite(inWcd, function(err, wcd) { + if (err) return cb(err); + + self.storage.save(wcd, function(err) { + return cb(err, null); + }); + }); +} + API.prototype.generateKey = function(network, cb) { var self = this; network = network || 'livenet'; @@ -337,7 +368,8 @@ API.prototype.generateKey = function(network, cb) { return cb(self.storage.getName() + ' already contains a wallet'); var wcd = _initWcd(network); - self.storage.save(wcd, function(err) { + + self.save(wcd, function(err) { return cb(err, null); }); }); @@ -378,7 +410,7 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[0], copayerName, function(err, wallet) { if (err) return cb(err); - self.storage.save(wcd, function(err) { + self.save(wcd, function(err) { return cb(err, n > 1 ? secret : null); }); }); @@ -439,7 +471,7 @@ API.prototype.joinWallet = function(secret, copayerName, cb) { function(err, joinedWallet) { if (err) return cb(err); _addWalletToWcd(wcd, secretData.walletPrivKey, joinedWallet.m, joinedWallet.n); - self.storage.save(wcd, cb); + self.save(wcd, cb); }); }); }; @@ -639,7 +671,7 @@ API.prototype.import = function(str, cb) { return cb('Invalid source wallet'); wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; - self.storage.save(wcd, function(err) { + self.save(wcd, function(err) { return cb(err, WalletUtils.accessFromData(wcd)); }); }); diff --git a/lib/walletutils.js b/lib/walletutils.js index 53b6eb6..88de849 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -40,7 +40,10 @@ WalletUtils.accessFromData = function(data) { if (data.rwPrivKey) return 'readwrite'; - return 'readonly'; + if (data.roPrivKey) + return 'readonly'; + + return 'none'; }; WalletUtils.accessNameToLevel = function(name) { @@ -51,13 +54,12 @@ WalletUtils.accessNameToLevel = function(name) { return 20; if (name === 'readonly') return 10; - + if (name === 'none') + return 0; throw new Error('Bad access name:' + name); }; -WalletUtils.isAccessEncrypted = function(name) {}; - WalletUtils.verifyMessage = function(text, signature, pubKey) { $.checkArgument(text); $.checkArgument(pubKey); @@ -160,7 +162,8 @@ WalletUtils.privateKeyToAESKey = function(privKey) { WalletUtils.decryptWallet = function(data, password) { $.checkArgument(data.enc); - var extraFields = sjcl.decrypt(password, data.enc); + var extraFields = JSON.parse(sjcl.decrypt(password, data.enc)); + delete data.enc; return _.extend(data, extraFields); }; @@ -171,24 +174,26 @@ WalletUtils.sjclOpts = { WalletUtils.encryptWallet = function(data, accessWithoutEncrytion, password) { - var toEncryptByLevel = { - readwrite: [], - readonly: [], + // Fields to encrypt, given the NOPASSWD access level + var fieldsEncryptByLevel = { + none: _.keys(data), + readonly: ['xPrivKey', 'rwPrivKey', 'publicKeyRing' ], + readwrite: ['xPrivKey', ], full: [], }; - var toEncrypt = whatToEncryptByLevel[accessWithoutEncrytion]; + var fieldsEncrypt = fieldsEncryptByLevel[accessWithoutEncrytion]; + $.checkState(!_.isUndefined(fieldsEncrypt)); - if (!_.every(toEncrypt, function(k) { + if (!_.every(fieldsEncrypt, function(k) { return data[k]; })) throw new Error('Wallet does not contain necesary info to encrypt'); - var toEncrypt = _.pick(data, whatToEncrypt); + var toEncrypt = _.pick(data, fieldsEncrypt); var enc = sjcl.encrypt(password, JSON.stringify(toEncrypt), WalletUtils.sjclOpts); - var ret = _.omit(data, toEncrypt); + var ret = _.omit(data, fieldsEncrypt); ret.enc = enc; -console.log('[walletutils.js.191:ret:]',ret); //TODO return ret; }; diff --git a/package.json b/package.json index 7671316..4216d69 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "npmlog": "^0.1.1", "preconditions": "^1.0.7", "qr-image": "*", + "read": "^1.0.5", "request": "^2.53.0", "sjcl": "^1.0.2", "uuid": "*" diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 4c38943..c132c0d 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -221,20 +221,105 @@ describe('client API ', function() { }); - describe.skip('Storage Encryption', function() { - it('should check balance in a 1-1 ', function(done) { + describe('Storage Encryption', function() { + beforeEach(function() { + _.each(_.range(3), function(i) { + clients[i].on('needPassword', function(cb) { + return cb('1234#$@#%F,./.**'); + }); + }); + }); + + + it('full encryption roundtrip', function(done) { + clients[0].setNopasswdAccess('none'); helpers.createAndJoinWallet(clients, 1, 1, function(err) { should.not.exist(err); - clients[0].getBalance(function(err, x) { + // Load it + var wcd = JSON.parse(fsmock._get('client0')); + fsmock._set('client1', wcd); + clients[1].getBalance(function(err, bal0) { should.not.exist(err); - - var wcd = JSON.parse(fsmock._get('client0')); - console.log('[clientApi.js.236]', wcd); //TODO done(); - }) + }); }); }); + + it('should fail if wrong password', function(done) { + clients[0].setNopasswdAccess('none'); + helpers.createAndJoinWallet(clients, 1, 1, function(err) { + should.not.exist(err); + + // Load it + var wcd = JSON.parse(fsmock._get('client0')); + fsmock._set('client4', wcd); + + clients[4].on('needPassword', function(cb) { + return cb('1'); + }); + + clients[4].getBalance(function(err, bal0) { + err.should.equal('NOTAUTH'); + done(); + }); + }); + }); + + + it('should encrypt everything', function(done) { + clients[0].setNopasswdAccess('none'); + helpers.createAndJoinWallet(clients, 1, 1, function(err) { + should.not.exist(err); + var wcd = JSON.parse(fsmock._get('client0')); + _.keys(wcd).should.deep.equal(['enc']); + done(); + }); + }); + + it('should encrypt xpriv access', function(done) { + clients[0].setNopasswdAccess('readwrite'); + helpers.createAndJoinWallet(clients, 1, 1, function(err) { + should.not.exist(err); + var wcd = JSON.parse(fsmock._get('client0')); + should.exist(wcd.enc); + should.not.exist(wcd.xpriv); + done(); + }); + }); + + it('should encrypt rwkey', function(done) { + clients[0].setNopasswdAccess('readonly'); + helpers.createAndJoinWallet(clients, 1, 1, function(err) { + should.not.exist(err); + var wcd = JSON.parse(fsmock._get('client0')); + should.exist(wcd.enc); + should.not.exist(wcd.xpriv); + should.not.exist(wcd.rwPrivKey); + done(); + }); + }); + + + _.each(['full', 'readwrite', 'readonly', 'none'], function(k) { + it('full encryption roundtrip: type:' + k, function(done) { + clients[0].setNopasswdAccess(k); + helpers.createAndJoinWallet(clients, 1, 1, function(err) { + should.not.exist(err); + + // Load it + var wcd = JSON.parse(fsmock._get('client0')); + fsmock._set('client1', wcd); + clients[1].getBalance(function(err, bal0) { + should.not.exist(err); + done(); + }); + }); + }); + }); + + it.skip('should not ask for password if not needed (readonly)', function(done) {}); + it.skip('should not ask for password if not needed (readwrite)', function(done) {}); }); @@ -460,7 +545,6 @@ describe('client API ', function() { }; clients[1].signTxProposal(x, function(err, tx) { -console.log('[clientApi.js.456:err:]',err); //TODO err.code.should.be.equal('BADSIGNATURES'); clients[1].getTxProposals({}, function(err, txs) { should.not.exist(err);