diff --git a/bit-wallet/bit-address b/bit-wallet/bit-address index 0ffe053..057a25c 100755 --- a/bit-wallet/bit-address +++ b/bit-wallet/bit-address @@ -9,8 +9,9 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -client.createAddress(function(err, x) { - utils.die(err); - console.log('* New Address %s ', x.address); +utils.getClient(program, function (client) { + client.createAddress(function(err, x) { + utils.die(err); + console.log('* New Address %s ', x.address); + }); }); diff --git a/bit-wallet/bit-addresses b/bit-wallet/bit-addresses index 5987e6b..a746030 100755 --- a/bit-wallet/bit-addresses +++ b/bit-wallet/bit-addresses @@ -10,14 +10,20 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -client.getMainAddresses({ - doNotVerify: true -}, function(err, x) { - utils.die(err); - console.log('* Addresses:'); - _.each(x, function(a) { - console.log(' ', a.address); +utils.getClient(program, function (client) { + client.getMainAddresses({ + doNotVerify: true + }, function(err, x) { + utils.die(err); + + if (x.length > 0) { + console.log('* Addresses:'); + _.each(x, function(a) { + console.log(' ', a.address); + }); + } else { + console.log('* No addresses.'); + } }); }); diff --git a/bit-wallet/bit-balance b/bit-wallet/bit-balance index 9f9cc40..c32e8b2 100755 --- a/bit-wallet/bit-balance +++ b/bit-wallet/bit-balance @@ -9,9 +9,10 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -client.getBalance(function(err, x) { - utils.die(err); - console.log('* Wallet balance %s (Locked %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount) ); +utils.getClient(program, function (client) { + client.getBalance(function(err, x) { + utils.die(err); + console.log('* Wallet balance %s (Locked %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount) ); + }); }); diff --git a/bit-wallet/bit-broadcast b/bit-wallet/bit-broadcast index cb94701..a186d19 100755 --- a/bit-wallet/bit-broadcast +++ b/bit-wallet/bit-broadcast @@ -11,19 +11,16 @@ program .parse(process.argv); var args = program.args; -if (!args[0]) - program.help(); +var txpid = args[0] || ''; -var txpid = args[0]; -var client = utils.getClient(program); - -client.getTxProposals({}, function(err, txps) { - utils.die(err); - - var txp = utils.findOneTxProposal(txps, txpid); - client.broadcastTxProposal(txp, function(err, txid) { +utils.getClient(program, function (client) { + client.getTxProposals({}, function(err, txps) { utils.die(err); - console.log('Transaction Broadcasted: TXID: ' + x.txid); - + + var txp = utils.findOneTxProposal(txps, txpid); + client.broadcastTxProposal(txp, function(err, txp) { + utils.die(err); + console.log('Transaction Broadcasted: TXID: ' + txp.txid); + }); }); }); diff --git a/bit-wallet/bit-confirm b/bit-wallet/bit-confirm index 99d4386..9d3002e 100755 --- a/bit-wallet/bit-confirm +++ b/bit-wallet/bit-confirm @@ -9,24 +9,27 @@ program = utils.configureCommander(program); program .parse(process.argv); -var client = utils.getClient(program); - -client.getStatus(function(err, x, myCopayerId) { - utils.die(err); - console.log('\n To be sure that none Copayer has joined more that once to this wallet, you can asked them their confirmation number. They can grab them using this (bit confirm) command.'); - - console.log('\n * Copayer confirmations ids:'); - - var myConfirmationId; - _.each(x.wallet.copayers, function(x) { - var confirmationId = utils.confirmationId(x); - if (x.id != myCopayerId) - console.log('\t\t* %s : %s', x.name, confirmationId); - else - myConfirmationId = confirmationId; +utils.getClient(program, function (client) { + client.getStatus(function(err, x) { + utils.die(err); + + if (x.wallet.n == 1) { + console.log('Confirmations only work on shared wallets'); + process.exit(1); + } + console.log('\n To be sure that no copayer has joined this wallet more than once, you can asked them for their confirmation number. They can get theirs by running the bit-confirm command.'); + console.log('\n * Copayer confirmation IDs:'); + + var myConfirmationId; + _.each(x.wallet.copayers, function(x) { + var confirmationId = utils.confirmationId(x); + if (x.id != client.credentials.copayerId) + console.log('\t\t* %s : %s', x.name, confirmationId); + else + myConfirmationId = confirmationId; + }); + + console.log('\t\t---'); + console.log('\t\tYour confirmation ID: %s', myConfirmationId); }); - - - console.log('\t\t---'); - console.log('\t\tYour confirmation ID: %s', myConfirmationId); }); diff --git a/bit-wallet/bit-create b/bit-wallet/bit-create index ebbe5ee..6fb2bdd 100755 --- a/bit-wallet/bit-create +++ b/bit-wallet/bit-create @@ -8,7 +8,6 @@ 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); @@ -27,10 +26,14 @@ try { utils.die(ex); } -var client = utils.getClient(program); -client.createWallet(walletName, copayerName, mn[0], mn[1], network, function(err, secret) { - utils.die(err); - console.log(' * ' + _.capitalize(network) + ' Wallet Created.'); - if (secret) - console.log(' - Secret to share:\n\t' + secret); +utils.getClient(program, function (client) { + client.createWallet(walletName, copayerName, mn[0], mn[1], network, function(err, secret) { + utils.die(err); + console.log(' * ' + _.capitalize(network) + ' Wallet Created.'); + utils.saveClient(program, client, function () { + if (secret) { + console.log(' - Secret to share:\n\t' + secret); + } + }); + }); }); diff --git a/bit-wallet/bit-history b/bit-wallet/bit-history index 7f250c2..f2bb8db 100755 --- a/bit-wallet/bit-history +++ b/bit-wallet/bit-history @@ -4,40 +4,39 @@ var _ = require('lodash'); var fs = require('fs'); var moment = require('moment'); var program = require('commander'); -var Utils = require('./cli-utils'); -program = Utils.configureCommander(program); +var utils = require('./cli-utils'); +program = utils.configureCommander(program); program .parse(process.argv); var args = program.args; -var client = Utils.getClient(program); -var txData; - -client.getTxHistory({}, function (err, txs) { - if (_.isEmpty(txs)) - return; - - console.log("* TX History:") - - _.each(txs, function(tx) { - var time = moment(tx.time * 1000).fromNow(); - var amount = Utils.renderAmount(tx.amount); - var confirmations = tx.confirmations || 0; - var proposal = tx.proposalId ? '["' + tx.message + '" by ' + tx.creatorName + '] ' : ''; - switch (tx.action) { - case 'received': - direction = '<='; - break; - case 'moved': - direction = '=='; - break; - case 'sent': - direction = '=>'; - break; - } - - console.log("\t%s: %s %s %s %s(%s confirmations)", time, direction, tx.action, amount, proposal, confirmations); +utils.getClient(program, function (client) { + client.getTxHistory({}, function (err, txs) { + if (_.isEmpty(txs)) + return; + + console.log("* TX History:") + + _.each(txs, function(tx) { + var time = moment(tx.time * 1000).fromNow(); + var amount = utils.renderAmount(tx.amount); + var confirmations = tx.confirmations || 0; + var proposal = tx.proposalId ? '["' + tx.message + '" by ' + tx.creatorName + '] ' : ''; + switch (tx.action) { + case 'received': + direction = '<='; + break; + case 'moved': + direction = '=='; + break; + case 'sent': + direction = '=>'; + break; + } + + console.log("\t%s: %s %s %s %s(%s confirmations)", time, direction, tx.action, amount, proposal, confirmations); + }); }); }); diff --git a/bit-wallet/bit-join b/bit-wallet/bit-join index fb169b6..1429b38 100755 --- a/bit-wallet/bit-join +++ b/bit-wallet/bit-join @@ -7,7 +7,6 @@ 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; @@ -17,8 +16,10 @@ if (!args[0]) var secret = args[0]; var copayerName = args[1] || process.env.USER; -var client = utils.getClient(program); -client.joinWallet(secret, copayerName, function(err, xx) { - utils.die(err); - console.log(' * Wallet Joined.', xx || ''); +utils.getClient(program, function (client) { + client.joinWallet(secret, copayerName, function(err, wallet) { + utils.die(err); + console.log(' * Wallet Joined.', wallet.name); + utils.saveClient(program, client, function () {}); + }); }); diff --git a/bit-wallet/bit-reject b/bit-wallet/bit-reject index de0858c..bdf1fd6 100755 --- a/bit-wallet/bit-reject +++ b/bit-wallet/bit-reject @@ -13,17 +13,18 @@ program var args = program.args; var txpid = args[0] || ''; var reason = args[1] || ''; -var client = utils.getClient(program); -client.getTxProposals({}, function(err, txps) { - utils.die(err); - - var txp = utils.findOneTxProposal(txps, txpid); - client.rejectTxProposal(txp, reason, function(err, tx) { +utils.getClient(program, function (client) { + client.getTxProposals({}, function(err, txps) { utils.die(err); - if (tx.status == 'rejected') - console.log('Transaction finally rejected.'); - else - console.log('Transaction rejected by you.'); + + var txp = utils.findOneTxProposal(txps, txpid); + client.rejectTxProposal(txp, reason, function(err, tx) { + utils.die(err); + if (tx.status == 'rejected') + console.log('Transaction finally rejected.'); + else + console.log('Transaction rejected by you.'); + }); }); }); diff --git a/bit-wallet/bit-rm b/bit-wallet/bit-rm index a27a787..b0472aa 100755 --- a/bit-wallet/bit-rm +++ b/bit-wallet/bit-rm @@ -12,26 +12,20 @@ program .parse(process.argv); var args = program.args; -if (!args[0]) - program.help(); +var txpid = args[0] || ''; -var txpid = args[0]; - -var cli = new Client({ - filename: program.config -}); - -cli.getTxProposals({}, function(err, txps) { - utils.die(err); - - if (program.verbose) - console.log('* Raw Server Response:\n', txps); //TODO +utils.getClient(program, function (client) { + client.getTxProposals({}, function(err, txps) { + utils.die(err); - var txp = utils.findOneTxProposal(txps, txpid); + if (program.verbose) + console.log('* Raw Server Response:\n', txps); //TODO - cli.removeTxProposal(txp, function(err) { - utils.die(err); + var txp = utils.findOneTxProposal(txps, txpid); + client.removeTxProposal(txp, function(err) { + utils.die(err); - console.log('Transaction removed.'); + console.log('Transaction removed.'); + }); }); }); diff --git a/bit-wallet/bit-send b/bit-wallet/bit-send index b8b8194..780eba5 100755 --- a/bit-wallet/bit-send +++ b/bit-wallet/bit-send @@ -31,14 +31,14 @@ try { } var note = args[2]; -var client = utils.getClient(program); - -client.sendTxProposal({ - toAddress: address, - amount: amount, - message: note -}, function(err, x) { - utils.die(err); - console.log(' * Tx created: ID %s [%s] RequiredSignatures:', - x.id, x.status, x.requiredSignatures); +utils.getClient(program, function (client) { + client.sendTxProposal({ + toAddress: address, + amount: amount, + message: note + }, function(err, x) { + utils.die(err); + console.log(' * Tx created: ID %s [%s] RequiredSignatures:', + x.id, x.status, x.requiredSignatures); + }); }); diff --git a/bit-wallet/bit-sign b/bit-wallet/bit-sign index 9faad47..b11c32d 100755 --- a/bit-wallet/bit-sign +++ b/bit-wallet/bit-sign @@ -15,9 +15,7 @@ program var args = program.args; var txpid = args[0] || ''; -var client = utils.getClient(program); - -function end(txp) { +function end(client, txp) { if (program.output) { client.getSignatures(txp, function(err, signatures) { utils.die(err); @@ -50,14 +48,16 @@ function end(txp) { }; -if (program.input && program.output) { - var inFile = JSON.parse(fs.readFileSync(program.input)); - end(inFile.txps[0]); -} else { - client.getTxProposals({}, function(err, txps) { - utils.die(err); - var txp = utils.findOneTxProposal(txps, txpid); - utils.die(err); - end(txp); - }); -} +utils.getClient(program, function (client) { + if (program.input && program.output) { + var inFile = JSON.parse(fs.readFileSync(program.input)); + end(client, inFile.txps[0]); + } else { + client.getTxProposals({}, function(err, txps) { + utils.die(err); + var txp = utils.findOneTxProposal(txps, txpid); + utils.die(err); + end(client, txp); + }); + } +}); diff --git a/bit-wallet/bit-status b/bit-wallet/bit-status index 43faaa5..4f29ce4 100755 --- a/bit-wallet/bit-status +++ b/bit-wallet/bit-status @@ -9,20 +9,20 @@ program .parse(process.argv); var args = program.args; -var client = utils.getClient(program); +utils.getClient(program, function (client) { + client.getStatus(function(err, res) { + utils.die(err); -client.getStatus(function(err, res) { - utils.die(err); + var x = res.wallet; + console.log('* Wallet %s [%s]: %d-of-%d %s ', x.name, x.network, x.m, x.n, x.status); - var x = res.wallet; - console.log('* Wallet %s [%s]: %d-%d %s ', x.name, x.network, x.m, x.n, x.status); + if (x.status != 'complete') + console.log(' Missing copayers:', x.n - x.copayers.length); + console.log('* Copayers:', _.pluck(x.copayers,'name').join(', ')); - if (x.status != 'complete') - console.log(' Missing copayers:', x.n - x.copayers.length); - console.log('* Copayers:', _.pluck(x.copayers,'name').join(', ')); + var x = res.balance; + console.log('* Balance %s (Locked: %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount)); - var x = res.balance; - console.log('* Balance %s (Locked: %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount)); - - utils.renderTxProposals(res.pendingTxps); + utils.renderTxProposals(res.pendingTxps); + }); }); diff --git a/bit-wallet/bit-txproposals b/bit-wallet/bit-txproposals index 26a27ae..2bcef75 100755 --- a/bit-wallet/bit-txproposals +++ b/bit-wallet/bit-txproposals @@ -7,39 +7,20 @@ var utils = require('./cli-utils'); program = utils.configureCommander(program); program - .option('-i, --input [filename]', 'use input file instead of server\'s') - .option('-o, --output [filename]', 'write tx to output file') + .option('-o, --output [filename]', 'write tx to output file for offline signing') .parse(process.argv); var args = program.args; -var client = utils.getClient(program); -var txData; - -function end(err, txps, rawtxps) { - utils.die(err); - if (program.input) { - console.log('\n* From File : %s\n', program.input); - } - utils.renderTxProposals(txps); - if (program.output) { - - client.getEncryptedWalletData(function (err, toComplete) { - var txData = { - toComplete: toComplete, - txps: txps, - }; - fs.writeFileSync(program.output, JSON.stringify(txData)); - console.log(' * Proposals Saved to: %s\n', program.output); - }); - } -}; - - -if (program.input) { - var txData = fs.readFileSync(program.input); - txData = JSON.parse(txData); - client.parseTxProposals(txData, end); -} else { - client.getTxProposals({getRawTxps: !!program.output}, end); -} +utils.getClient(program, function (client) { + client.getTxProposals({forAirGapped: !!program.output}, function (err, res) { + utils.die(err); + + if (program.output) { + fs.writeFileSync(program.output, JSON.stringify(res)); + console.log(' * Tx proposals saved to: %s\n', program.output); + } else { + utils.renderTxProposals(res); + } + }); +}); diff --git a/bit-wallet/cli-utils.js b/bit-wallet/cli-utils.js index f6c65e8..b621c24 100644 --- a/bit-wallet/cli-utils.js +++ b/bit-wallet/cli-utils.js @@ -1,6 +1,8 @@ var _ = require('lodash'); var Client = require('../lib/client'); +var FileStorage = require('./filestorage'); var read = require('read') +var log = require('npmlog'); var Utils = function() {}; @@ -39,55 +41,75 @@ Utils.confirmationId = function(copayer) { return parseInt(copayer.xPubKeySignature.substr(-4), 16).toString().substr(-4); } -Utils.getClient = function(args) { - var storage = new Client.FileStorage({ +Utils.getClient = function(args, cb) { + var storage = new FileStorage({ filename: args.file || process.env['BIT_FILE'], }); - var c = new Client({ - storage: storage, + var client = new Client({ baseUrl: args.host || process.env['BIT_HOST'], verbose: args.verbose, }); - - - if (args.nopasswd) - c.setNopasswdAccess(args.nopasswd); - - var setPassword; - c.on('needPassword', function(cb) { - if (args.password) { - return cb(args.password); - } else { - if (setPassword) - return cb(setPassword); - - read({ - prompt: 'Password for ' + args.file + ' : ', - silent: true - }, function(er, password) { - setPassword = password; - return cb(password); - }) - } + storage.load(function(err, walletData) { + if (err && err.code != 'ENOENT') die(err); + if (!walletData) return cb(client); + + client.import(walletData); + client.openWallet(function(err, justCompleted) { + if (client.isComplete() && justCompleted) { + Utils.saveClient(args, client, function() { + log.info('Your wallet has just been completed. Please backup your wallet file or use the export command.'); + return cb(client); + }); + } else { + return cb(client); + } + }); }); +}; - c.on('needNewPassword', function(cb) { - if (args.password) { - return cb(args.password); - } else { - read({ - prompt: 'New Password: ', - silent: true - }, function(er, password) { - return cb(password); - }) - } +Utils.saveClient = function(args, client, cb) { + var storage = new FileStorage({ + filename: args.file || process.env['BIT_FILE'], }); + var str = client.export(); + storage.save(str, function(err) { + die(err); + return cb(); + }); +}; +// var setPassword; +// c.on('needPassword', function(cb) { +// if (args.password) { +// return cb(args.password); +// } else { +// if (setPassword) +// return cb(setPassword); + +// read({ +// prompt: 'Password for ' + args.file + ' : ', +// silent: true +// }, function(er, password) { +// setPassword = password; +// return cb(password); +// }) +// } +// }); + +// c.on('needNewPassword', function(cb) { +// if (args.password) { +// return cb(args.password); +// } else { +// read({ +// prompt: 'New Password: ', +// silent: true +// }, function(er, password) { +// return cb(password); +// }) +// } +// }); - return c; -} Utils.findOneTxProposal = function(txps, id) { var matches = _.filter(txps, function(tx) { @@ -173,7 +195,11 @@ Utils.renderTxProposals = function(txps) { return a.copayerName + ' ' + (a.type == 'accept' ? '✓' : '✗') + (a.comment ? ' (' + a.comment + ')' : ''); }).join('. ')); } - console.log('\t\tMissing signatures: ' + missingSignatures); + if (missingSignatures > 0) { + console.log('\t\tMissing signatures: ' + missingSignatures); + } else { + console.log('\t\tReady to broadcast'); + } }); }; diff --git a/lib/client/filestorage.js b/bit-wallet/filestorage.js similarity index 86% rename from lib/client/filestorage.js rename to bit-wallet/filestorage.js index 17769da..021a774 100644 --- a/lib/client/filestorage.js +++ b/bit-wallet/filestorage.js @@ -1,4 +1,3 @@ - var fs = require('fs') function FileStorage(opts) { @@ -18,16 +17,14 @@ FileStorage.prototype.save = function(data, cb) { }; FileStorage.prototype.load = function(cb) { - this.fs.readFile(this.filename, 'utf8', function(err,data) { + this.fs.readFile(this.filename, 'utf8', function(err, data) { if (err) return cb(err); try { data = JSON.parse(data); - } catch (e) { - } + } catch (e) {} return cb(null, data); }); }; module.exports = FileStorage; - diff --git a/lib/client/airgapped.js b/lib/client/airgapped.js new file mode 100644 index 0000000..8bde1e0 --- /dev/null +++ b/lib/client/airgapped.js @@ -0,0 +1,62 @@ +'use strict'; + +var _ = require('lodash'); +var $ = require('preconditions').singleton(); +var util = require('util'); +var async = require('async'); +var log = require('npmlog'); +var events = require('events'); +log.debug = log.verbose; +var Bitcore = require('bitcore') + +var Credentials = require('./credentials'); +var WalletUtils = require('../walletutils'); +var Verifier = require('./verifier'); +var ServerCompromisedError = require('./servercompromisederror'); +var ClientError = require('../clienterror'); + +function AirGapped(opts) { + this.verbose = !!opts.verbose; + if (this.verbose) { + log.level = 'debug'; + } else { + log.level = 'info'; + } + this.credentials = Credentials.create(opts.network || 'livenet'); +}; + +util.inherits(AirGapped, events.EventEmitter); + +AirGapped.prototype.getSeed = function() { + return { + xPubKey: this.credentials.xPubKey, + requestPrivKey: this.credentials.requestPrivKey, + }; +}; + +AirGapped.prototype.signTxProposal = function(txp, encryptedPkr, m, n) { + var self = this; + + var publicKeyRing; + try { + publicKeyRing = JSON.parse(WalletUtils.decryptMessage(encryptedPkr, self.credentials.personalEncryptingKey)); + } catch (ex) { + console.log(ex); + throw new Error('Could not decrypt public key ring'); + } + + if (!_.isArray(publicKeyRing) || publicKeyRing.length != n) { + throw new Error('Invalid public key ring'); + } + + self.credentials.m = m; + self.credentials.n = n; + self.credentials.addPublicKeyRing(publicKeyRing); + + if (!Verifier.checkTxProposal(self.credentials, txp)) { + throw new Error('Fake transaction proposal'); + } + return WalletUtils.signTxp(txp, self.credentials.xPrivKey); +}; + +module.exports = AirGapped; diff --git a/lib/client/api.js b/lib/client/api.js index 2abb69c..82fbcee 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -8,19 +8,19 @@ var log = require('npmlog'); var request = require('request') var events = require('events'); log.debug = log.verbose; - var Bitcore = require('bitcore') +var sjcl = require('sjcl'); + +var Credentials = require('./credentials'); var WalletUtils = require('../walletutils'); var Verifier = require('./verifier'); var ServerCompromisedError = require('./servercompromisederror'); var ClientError = require('../clienterror'); var BASE_URL = 'http://localhost:3001/copay/api'; - -var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'n', 'publicKeyRing', 'sharedEncryptingKey']; -var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; - -var WALLET_AIRGAPPED_TOCOMPLETE = ['publicKeyRing', 'm', 'n', 'sharedEncryptingKey']; +var WALLET_ENCRYPTION_OPTS = { + iter: 5000 +}; function _encryptMessage(message, encryptingKey) { if (!message) return null; @@ -75,45 +75,13 @@ 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) { - throw new Error('Must provide storage option'); - } - this.storage = opts.storage; + opts = opts || {}; + this.verbose = !!opts.verbose; - this.request = request || opts.request; + this.request = opts.request || 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 { @@ -123,176 +91,90 @@ function API(opts) { util.inherits(API, events.EventEmitter); -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, 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, wcd.walletPrivKey, - wcd.xPrivKey, wcd.n)) { - - return cb(new ServerCompromisedError( - 'Copayers in the wallet could not be verified to have known the wallet secret')); - } - - wcd.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') - - self.save(wcd, function(err) { - return cb(err, wcd); - }); - }); -}; - - -API.prototype._tryToCompleteFromData = function(wcd, toComplete, cb) { - var inData = _decryptMessage(toComplete, - WalletUtils.privateKeyToAESKey(wcd.roPrivKey)); - if (!inData) - return cb('Could not complete wallet'); - - try { - inData = JSON.parse(inData); - _.extend(wcd, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); - } catch (ex) { - return cb(ex); - } - - this.save(wcd, function(err) { - return cb(err, wcd); - }); -}; - - -API.prototype._tryToComplete = function(opts, wcd, cb) { - if (opts.toComplete) { - this._tryToCompleteFromData(wcd, opts.toComplete, cb); - } else { - this._tryToCompleteFromServer(wcd, cb); - } +API.prototype.seedFromExtendedPrivateKey = function(xPrivKey) { + this.credentials = Credentials.fromExtendedPrivateKey(xPrivKey); }; - -// access: 'full' > 'readwrite' > readonly' -API.prototype._processWcdAfterRead = function(rawData, requiredAccess, cb) { - var WU = WalletUtils; - requiredAccess = requiredAccess || 'full'; - - if (!rawData) - return cb(null, rawData); - - var requiredAccessLevel = WU.accessNameToLevel(requiredAccess); - - var access = WU.accessFromData(rawData); - var accessLevel = WU.accessNameToLevel(access); - - // Is the data available? - if (requiredAccessLevel <= accessLevel) - return cb(null, rawData); - - // Has any encrypted info? - if (!rawData.enc) - return cb('NOTAUTH'); - - // Decrypt it and try again - this.emit('needPassword', function(password) { - if (!password) return cb('No password'); - - 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) - return cb(null, rawData); - - return cb('NOTAUTH'); - }); +API.prototype.seedFromAirGapped = function(seed) { + this.credentials = Credentials.fromExtendedPublicKey(seed.xPubKey, seed.requestPrivKey); }; +/** + * export + * + * @param opts + * @param opts.compressed + * @param opts.password + */ +API.prototype.export = function(opts) { + $.checkState(this.credentials); -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; + opts = opts || {}; - // Is any encrypted? - if (this.noPasswdAccess == 'full') { - return cb(null, wcd); + var output; + if (opts.compressed) { + output = this.credentials.exportCompressed(); } else { - this.emit('needNewPassword', function(password) { - if (!password) return cb('No password given'); - var ewcd = WalletUtils.encryptWallet(wcd, self.noPasswdAccess, password); - return cb(null, ewcd); - }); + output = JSON.stringify(this.credentials.toObj()); } -}; - - -API.prototype._load = function(opts, cb) { - var self = this; - $.shouldBeFunction(cb); - - this.storage.load(function(err, rawdata) { - if (err || !rawdata) { - if (err && err.code == 'ENOENT') err = 'NOTFOUND'; - return cb(err || 'NOTFOUND'); - } + if (opts.password) { + output = sjcl.encrypt(opts.password, output, WALLET_ENCRYPTION_OPTS); + } - self._processWcdAfterRead(rawdata, opts.requiredAccess, cb); - }); -}; + return output; +} /** - * _loadAndCheck + * export * - * @param opts.pkr + * @param opts + * @param opts.compressed + * @param opts.password */ -API.prototype._loadAndCheck = function(opts, cb) { - var self = this; +API.prototype.import = function(str, opts) { + opts = opts || {}; - this._load(opts, function(err, wcd) { - if (err) return cb(err); + var input = str; + if (opts.password) { + try { + input = sjcl.decrypt(opts.password, input); + } catch (ex) { + throw new Error('Incorrect password'); + } + } - if (!wcd.n || (wcd.n > 1 && wcd.publicKeyRing.length != wcd.n)) { - return self._tryToComplete(opts, wcd, cb); + var credentials; + try { + if (opts.compressed) { + credentials = Credentials.importCompressed(input); + // TODO: complete missing fields that live on the server only such as: walletId, walletName, copayerName + } else { + credentials = Credentials.fromObj(JSON.parse(input)); } + } catch (ex) { + throw new Error('Error importing from source'); + } + this.credentials = credentials; +}; - return cb(null, wcd); - }); +API.prototype.toString = function(password) { + $.checkState(this.credentials); + return this.credentials.toObject(); +}; + +API.prototype.fromString = function(str) { + this.credentials = Credentials.fromObject(str); }; -API.prototype._doRequest = function(method, url, args, wcd, cb) { +API.prototype._doRequest = function(method, url, args, cb) { + $.checkState(this.credentials); + var reqSignature; - wcd = wcd || {}; - if (method == 'get') { - if (wcd.roPrivKey) - reqSignature = _signRequest(method, url, args, wcd.roPrivKey); - } else { - if (wcd.rwPrivKey) - reqSignature = _signRequest(method, url, args, wcd.rwPrivKey); + if (this.credentials.requestPrivKey) { + reqSignature = _signRequest(method, url, args, this.credentials.requestPrivKey); } var absUrl = this.baseUrl + url; @@ -300,7 +182,7 @@ API.prototype._doRequest = function(method, url, args, wcd, cb) { // relUrl: only for testing with `supertest` relUrl: this.basePath + url, headers: { - 'x-identity': wcd.copayerId, + 'x-identity': this.credentials.copayerId, 'x-signature': reqSignature, }, method: method, @@ -308,6 +190,7 @@ API.prototype._doRequest = function(method, url, args, wcd, cb) { body: args, json: true, }; + log.verbose('Request Args', util.inspect(args, { depth: 10 })); @@ -326,14 +209,17 @@ API.prototype._doRequest = function(method, url, args, wcd, cb) { }; -API.prototype._doPostRequest = function(url, args, wcd, cb) { - return this._doRequest('post', url, args, wcd, cb); +API.prototype._doPostRequest = function(url, args, cb) { + return this._doRequest('post', url, args, cb); }; -API.prototype._doGetRequest = function(url, wcd, cb) { - return this._doRequest('get', url, {}, wcd, cb); +API.prototype._doGetRequest = function(url, cb) { + return this._doRequest('get', url, {}, cb); }; +API.prototype._doDeleteRequest = function(url, cb) { + return this._doRequest('delete', url, {}, cb); +}; API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) { var args = { @@ -343,166 +229,122 @@ API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayer xPubKeySignature: WalletUtils.signMessage(xPubKey, walletPrivKey), }; var url = '/v1/wallets/' + walletId + '/copayers'; - this._doPostRequest(url, args, {}, function(err, body) { + this._doPostRequest(url, args, function(err, body) { if (err) return cb(err); return cb(null, body.wallet); }); }; -API.prototype.save = function(inWcd, cb) { +API.prototype.isComplete = function() { + return this.credentials && this.credentials.isComplete(); +}; + +/** + * Opens a wallet and tries to complete the public key ring. + * @param {Function} cb - Returns an error and a flag indicating that the wallet has just been completed and needs to be persisted + */ +API.prototype.openWallet = function(cb) { + $.checkState(this.credentials); + var self = this; - self._processWcdBeforeWrite(inWcd, function(err, wcd) { - if (err) return cb(err); + if (self.credentials.isComplete()) return cb(null, false); - self.storage.save(wcd, function(err) { - return cb(err, null); - }); - }); -} + self._doGetRequest('/v1/wallets/', function(err, ret) { + if (err) return cb(err); + var wallet = ret.wallet; -API.prototype.generateKey = function(network, cb) { - var self = this; - network = network || 'livenet'; - if (!_.contains(['testnet', 'livenet'], network)) - return cb('Invalid network'); + if (wallet.status != 'complete') return cb('Wallet Incomplete'); - this.storage.load(function(err, wcd) { - if (wcd) - return cb(self.storage.getName() + ' already contains a wallet'); + if (!!self.credentials.walletPrivKey) { + if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { + return cb(new ServerCompromisedError( + 'Copayers in the wallet could not be verified to have known the wallet secret')); + } + } else { + log.warn('Could not perform verification of other copayers in the wallet'); + } - var wcd = _initWcd(network); + self.credentials.addPublicKeyRing(_.pluck(wallet.copayers, 'xPubKey')); + if (!self.credentials.hasWalletInfo()) { + var me = _.find(wallet.copayers, { + id: self.credentials.copayerId + }); + self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, null, me.name); + } - self.save(wcd, function(err) { - return cb(err, null); - }); + return cb(null, true); }); }; API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) { var self = this; - network = network || 'livenet'; - if (!_.contains(['testnet', 'livenet'], network)) - return cb('Invalid network'); - - this._load({ - requiredAccess: 'readonly', - }, function(err, wcd) { - if (err && err != 'NOTFOUND') - return cb(err); - - 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, - m: m, - n: n, - pubKey: walletPrivKey.toPublicKey().toString(), - network: network, - }; - var url = '/v1/wallets/'; - self._doPostRequest(url, args, {}, function(err, body) { - if (err) return cb(err); - var walletId = body.walletId; + network = network || 'livenet'; + if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); - var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); + if (!self.credentials) { + log.info('Generating new keys'); + self.credentials = Credentials.create(network); + } else { + log.info('Using existing keys'); + } - 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.save(wcd, function(err) { - return cb(err, n > 1 ? secret : null); - }); - }); - }); - }); -}; + $.checkState(network == self.credentials.network); + var walletPrivKey = new Bitcore.PrivateKey(); + var args = { + name: walletName, + m: m, + n: n, + pubKey: walletPrivKey.toPublicKey().toString(), + network: network, + }; -API.prototype.reCreateWallet = function(walletName, cb) { - var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { + self._doPostRequest('/v1/wallets/', args, function(err, body) { if (err) return cb(err); - var walletPrivKey = new Bitcore.PrivateKey(); - var args = { - name: walletName, - m: wcd.m, - n: wcd.n, - pubKey: walletPrivKey.toPublicKey().toString(), - network: wcd.network, - }; - var url = '/v1/wallets/'; - self._doPostRequest(url, args, {}, function(err, body) { - if (err) return cb(err); + var walletId = body.walletId; - var walletId = body.walletId; + var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); + self.credentials.addWalletInfo(walletId, walletName, m, n, walletPrivKey.toString(), copayerName); - var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network); - var i = 0; - async.each(wcd.publicKeyRing, function(xpub, next) { - var copayerName = 'recovered Copayer #' + i; - self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next); - }, function(err) { - return cb(err); + self._doJoinWallet(walletId, walletPrivKey, self.credentials.xPubKey, copayerName, + function(err, wallet) { + if (err) return cb(err); + return cb(null, n > 1 ? secret : null); }); - }); }); }; - API.prototype.joinWallet = function(secret, copayerName, cb) { var self = this; - this._load({ - requiredAccess: 'readonly' - }, function(err, wcd) { - if (err && err != 'NOTFOUND') - return cb(err); - - if (wcd && wcd.n) - return cb(self.storage.getName() + ' already contains a wallet'); + try { + var secretData = WalletUtils.fromSecret(secret); + } catch (ex) { + return cb(ex); + } - try { - var secretData = WalletUtils.fromSecret(secret); - } catch (ex) { - return cb(ex); - } - wcd = wcd || _initWcd(secretData.network); + if (!self.credentials) { + self.credentials = Credentials.create(secretData.network); + } - self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, wcd.publicKeyRing[0], copayerName, - function(err, joinedWallet) { - if (err) return cb(err); - _addWalletToWcd(wcd, secretData.walletPrivKey, joinedWallet.m, joinedWallet.n); - self.save(wcd, cb); - }); - }); + self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, self.credentials.xPubKey, copayerName, + function(err, wallet) { + if (err) return cb(err); + self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, secretData.walletPrivKey, copayerName); + return cb(null, wallet); + }); }; API.prototype.getStatus = function(cb) { + $.checkState(this.credentials); var self = this; - this._load({ - requiredAccess: 'readonly' - }, function(err, wcd) { - if (err) return cb(err); - - var url = '/v1/wallets/'; - self._doGetRequest(url, wcd, function(err, result) { - _processTxps(result.pendingTxps, wcd.sharedEncryptingKey); - return cb(err, result, wcd.copayerId); - }); + self._doGetRequest('/v1/wallets/', function(err, result) { + _processTxps(result.pendingTxps, self.credentials.sharedEncryptingKey); + return cb(err, result); }); }; @@ -515,50 +357,39 @@ API.prototype.getStatus = function(cb) { * @param opts.message */ API.prototype.sendTxProposal = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(opts); $.shouldBeNumber(opts.amount); var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { - if (err) return cb(err); - - if (!wcd.rwPrivKey) - return cb('No key to generate proposals'); - - var args = { - toAddress: opts.toAddress, - amount: opts.amount, - message: _encryptMessage(opts.message, wcd.sharedEncryptingKey), - }; - var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); - args.proposalSignature = WalletUtils.signMessage(hash, wcd.rwPrivKey); - log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); + var args = { + toAddress: opts.toAddress, + amount: opts.amount, + message: _encryptMessage(opts.message, self.credentials.sharedEncryptingKey), + }; + var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); + args.proposalSignature = WalletUtils.signMessage(hash, self.credentials.requestPrivKey); + log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); - var url = '/v1/txproposals/'; - self._doPostRequest(url, args, wcd, cb); + self._doPostRequest('/v1/txproposals/', args, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); }); }; API.prototype.createAddress = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({ - requiredAccess: 'readwrite', - }, function(err, wcd) { + self._doPostRequest('/v1/addresses/', {}, function(err, address) { if (err) return cb(err); + if (!Verifier.checkAddress(self.credentials, address)) { + return cb(new ServerCompromisedError('Server sent fake address')); + } - var url = '/v1/addresses/'; - self._doPostRequest(url, {}, wcd, function(err, address) { - if (err) return cb(err); - if (!Verifier.checkAddress(wcd, address)) { - return cb(new ServerCompromisedError('Server sent fake address')); - } - - return cb(null, address); - }); + return cb(null, address); }); }; @@ -567,344 +398,167 @@ API.prototype.createAddress = function(cb) { */ API.prototype.getMainAddresses = function(opts, cb) { - var self = this; - - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { - if (err) return cb(err); - - var url = '/v1/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(wcd, address); - }); - if (fake) - return cb(new ServerCompromisedError('Server sent fake address')); - } - return cb(null, addresses); - }); - }); -}; + $.checkState(this.credentials && this.credentials.isComplete()); -API.prototype.getBalance = function(cb) { var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - }, function(err, wcd) { + self._doGetRequest('/v1/addresses/', function(err, addresses) { if (err) return cb(err); - var url = '/v1/balance/'; - self._doGetRequest(url, wcd, cb); - }); -}; - -/** - * Export does not try to complete the wallet from the server. Exports the - * wallet as it is now. - * - * @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._load({ - requiredAccess: access, - }, function(err, wcd) { - if (err) return cb(err); - var v = []; - - var myXPubKey = wcd.xPrivKey ? (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString() : ''; - - _.each(WALLET_CRITICAL_DATA, function(k) { - var d; - 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(wcd[k], myXPubKey); - } else { - d = wcd[k]; - } - v.push(d); - }); - - if (access != 'full') { - v.push(wcd.copayerId); - v.push(wcd.roPrivKey); - if (access == 'readwrite') { - v.push(wcd.rwPrivKey); - } - } - - return cb(null, JSON.stringify(v)); - }); -} - - -API.prototype.import = function(str, cb) { - var self = this; - - this.storage.load(function(err, wcd) { - if (wcd) - return cb('Storage already contains a wallet'); - - wcd = {}; - - var inData = JSON.parse(str); - var i = 0; - - _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { - wcd[k] = inData[i++]; - }); - - if (wcd.xPrivKey) { - var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey); - var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); - 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(); + if (!opts.doNotVerify) { + var fake = _.any(addresses, function(address) { + return !Verifier.checkAddress(self.credentials, address); + }); + if (fake) + return cb(new ServerCompromisedError('Server sent fake address')); } - - if (!wcd.publicKeyRing) - return cb('Invalid source wallet'); - - wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; - - self.save(wcd, function(err) { - return cb(err, WalletUtils.accessFromData(wcd)); - }); + return cb(null, addresses); }); }; -/** - * - */ - -API.prototype.parseTxProposals = function(txData, cb) { +API.prototype.getBalance = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly', - toComplete: txData.toComplete - }, function(err, wcd) { - if (err) return cb(err); - - var txps = txData.txps; - _processTxps(txps, wcd.sharedEncryptingKey); - - var fake = _.any(txps, function(txp) { - return (!Verifier.checkTxProposal(wcd, txp)); - }); - - if (fake) - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - - return cb(null, txps); - }); + self._doGetRequest('/v1/balance/', cb); }; - /** * * opts.doNotVerify - * opts.getRawTxps + * opts.forAirGapped * @return {undefined} */ API.prototype.getTxProposals = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly' - }, function(err, wcd) { + self._doGetRequest('/v1/txproposals/', function(err, txps) { if (err) return cb(err); - var url = '/v1/txproposals/'; - 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, wcd.sharedEncryptingKey); - - var fake = _.any(txps, function(txp) { - return (!opts.doNotVerify && !Verifier.checkTxProposal(wcd, txp)); - }); + _processTxps(txps, self.credentials.sharedEncryptingKey); - if (fake) - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - - return cb(null, txps, rawTxps); + var fake = _.any(txps, function(txp) { + return (!opts.doNotVerify && !Verifier.checkTxProposal(self.credentials, txp)); }); - }); -}; -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(wcd.xPrivKey, network); + if (fake) + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - _.each(txp.inputs, function(i) { - if (!derived[i.path]) { - derived[i.path] = xpriv.derive(i.path).privateKey; - privs.push(derived[i.path]); + var result; + if (opts.forAirGapped) { + result = { + txps: JSON.parse(JSON.stringify(txps)), + publicKeyRing: WalletUtils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey), + m: self.credentials.m, + n: self.credentials.n, + }; + } else { + result = txps; } - }); - - 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 cb(null, result); }); - - return signatures; }; API.prototype.getSignatures = function(txp, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(txp.creatorId); - var self = this; - this._loadAndCheck({ - requiredAccess: 'full' - }, function(err, wcd) { - if (err) return cb(err); - - if (!Verifier.checkTxProposal(wcd, txp)) { - return cb(new ServerCompromisedError('Transaction proposal is invalid')); - } - - return cb(null, self._getSignaturesFor(txp, wcd)); - }); -}; - -API.prototype.getEncryptedWalletData = function(cb) { var self = this; - this._loadAndCheck({ - requiredAccess: 'readonly' - }, function(err, wcd) { - if (err) return cb(err); - var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE)); - return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey))); - }); -}; + if (!self.credentials.canSign()) + return cb('You do not have the required keys to sign transactions'); + if (!Verifier.checkTxProposal(self.credentials, txp)) { + return cb(new ServerCompromisedError('Transaction proposal is invalid')); + } + return cb(null, WalletUtils.signTxp(txp, self.credentials.xPrivKey)); +}; API.prototype.signTxProposal = function(txp, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(txp.creatorId); var self = this; - this._loadAndCheck({ - requiredAccess: txp.signatures ? 'readwrite' : 'full' - }, function(err, wcd) { - if (err) return cb(err); + if (!self.credentials.canSign() && !txp.signatures) + return cb(new Error('You do not have the required keys to sign transactions')); - if (!Verifier.checkTxProposal(wcd, txp)) { - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - } + if (!Verifier.checkTxProposal(self.credentials, txp)) { + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); + } - var signatures = txp.signatures || self._getSignaturesFor(txp, wcd); + var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey); - var url = '/v1/txproposals/' + txp.id + '/signatures/'; - var args = { - signatures: signatures - }; + var url = '/v1/txproposals/' + txp.id + '/signatures/'; + var args = { + signatures: signatures + }; - self._doPostRequest(url, args, wcd, cb); + self._doPostRequest(url, args, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); }); }; API.prototype.rejectTxProposal = function(txp, reason, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(cb); var self = this; - this._loadAndCheck({ - requiredAccess: 'readwrite' - }, - function(err, wcd) { - if (err) return cb(err); - - var url = '/v1/txproposals/' + txp.id + '/rejections/'; - var args = { - reason: _encryptMessage(reason, wcd.sharedEncryptingKey) || '', - }; - self._doPostRequest(url, args, wcd, cb); - }); + var url = '/v1/txproposals/' + txp.id + '/rejections/'; + var args = { + reason: _encryptMessage(reason, self.credentials.sharedEncryptingKey) || '', + }; + self._doPostRequest(url, args, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }); }; API.prototype.broadcastTxProposal = function(txp, cb) { - var self = this; + $.checkState(this.credentials && this.credentials.isComplete()); - this._loadAndCheck({ - requiredAccess: 'readwrite' - }, - function(err, wcd) { - if (err) return cb(err); + var self = this; - var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - self._doPostRequest(url, {}, wcd, cb); - }); + var url = '/v1/txproposals/' + txp.id + '/broadcast/'; + self._doPostRequest(url, {}, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }); }; API.prototype.removeTxProposal = function(txp, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({ - requiredAccess: 'readwrite' - }, - function(err, wcd) { - if (err) return cb(err); - var url = '/v1/txproposals/' + txp.id; - self._doRequest('delete', url, {}, wcd, cb); - }); + + var url = '/v1/txproposals/' + txp.id; + self._doDeleteRequest(url, function(err) { + if (err) return cb(err); + return cb(); + }); }; API.prototype.getTxHistory = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + var self = this; - this._loadAndCheck({}, function(err, wcd) { + self._doGetRequest('/v1/txhistory/', function(err, txs) { if (err) return cb(err); - var url = '/v1/txhistory/'; - self._doGetRequest(url, wcd, function(err, txs) { - if (err) return cb(err); - _processTxps(txs, wcd.sharedEncryptingKey); + _processTxps(txs, self.credentials.sharedEncryptingKey); - return cb(null, txs); - }); + return cb(null, txs); }); }; diff --git a/lib/client/credentials.js b/lib/client/credentials.js new file mode 100644 index 0000000..becf254 --- /dev/null +++ b/lib/client/credentials.js @@ -0,0 +1,169 @@ +'use strict'; + +var $ = require('preconditions').singleton(); +var _ = require('lodash'); +var Bitcore = require('bitcore'); +var WalletUtils = require('../walletutils'); + +var FIELDS = [ + 'network', + 'xPrivKey', + 'xPubKey', + 'requestPrivKey', + 'copayerId', + 'publicKeyRing', + 'walletId', + 'walletName', + 'm', + 'n', + 'walletPrivKey', + 'personalEncryptingKey', + 'sharedEncryptingKey', + 'copayerName', +]; + +var EXPORTABLE_FIELDS = [ + 'xPrivKey', + 'requestPrivKey', + 'xPubKey', + 'm', + 'n', + 'publicKeyRing', + 'sharedEncryptingKey', +]; + +function Credentials() { + this.version = '1.0.0'; +}; + +Credentials.create = function(network) { + var x = new Credentials(); + + x.network = network; + x.xPrivKey = (new Bitcore.HDPrivateKey(network)).toString(); + x._expand(); + return x; +}; + +Credentials.fromExtendedPrivateKey = function(xPrivKey) { + var x = new Credentials(); + x.xPrivKey = xPrivKey; + x._expand(); + return x; +}; + +Credentials.fromExtendedPublicKey = function(xPubKey, requestPrivKey) { + var x = new Credentials(); + x.xPubKey = xPubKey; + x.requestPrivKey = requestPrivKey; + x._expand(); + return x; +}; + +Credentials.prototype._expand = function() { + $.checkState(this.xPrivKey || this.xPubKey); + + if (this.xPrivKey) { + var xPrivKey = new Bitcore.HDPrivateKey.fromString(this.xPrivKey); + this.xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); + this.requestPrivKey = xPrivKey.derive('m/1/1').privateKey.toString(); + } + var network = WalletUtils.getNetworkFromXPubKey(this.xPubKey); + if (this.network) { + $.checkState(this.network == network); + } else { + this.network = network; + } + + this.personalEncryptingKey = WalletUtils.privateKeyToAESKey(this.requestPrivKey); + this.copayerId = WalletUtils.xPubToCopayerId(this.xPubKey); +}; + +Credentials.fromObj = function(obj) { + var x = new Credentials(); + + _.each(FIELDS, function(k) { + x[k] = obj[k]; + }); + + return x; +}; + +Credentials.prototype.toObj = function() { + return this; +}; + +Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, walletPrivKey, copayerName) { + this.walletId = walletId; + this.walletName = walletName; + this.m = m; + this.n = n; + this.walletPrivKey = walletPrivKey; + this.sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); + this.copayerName = copayerName; + if (n == 1) { + this.addPublicKeyRing([this.xPubKey]); + } +}; + +Credentials.prototype.hasWalletInfo = function() { + return !!this.walletId; +}; + +Credentials.prototype.addPublicKeyRing = function(publicKeyRing) { + this.publicKeyRing = _.clone(publicKeyRing); +}; + +Credentials.prototype.canSign = function() { + return !!this.xPrivKey; +}; + +Credentials.prototype.isComplete = function() { + if (!this.m || !this.n) return false; + if (!this.publicKeyRing || this.publicKeyRing.length != this.n) return false; + return true; +}; + +Credentials.prototype.exportCompressed = function() { + var self = this; + + var values = _.map(EXPORTABLE_FIELDS, function(field) { + if ((field == 'xPubKey' || field == 'requestPrivKey') && self.canSign()) return ''; + if (field == 'requestPrivKey') { + return Bitcore.PrivateKey.fromString(self.requestPrivKey).toWIF(); + } + if (field == 'publicKeyRing') { + return _.without(self.publicKeyRing, self.xPubKey); + } + return self[field]; + }); + values.unshift(self.version); + + return JSON.stringify(values); +}; + +Credentials.importCompressed = function(compressed) { + var list; + try { + list = JSON.parse(compressed); + } catch (ex) { + throw new Error('Invalid compressed format'); + } + + var x = new Credentials(); + + // Remove version + var version = list[0]; + list = _.rest(list); + + _.each(EXPORTABLE_FIELDS, function(field, i) { + x[field] = list[i]; + }); + x._expand(); + + x.network = WalletUtils.getNetworkFromXPubKey(x.xPubKey); + x.publicKeyRing.push(x.xPubKey); + return x; +}; + +module.exports = Credentials; diff --git a/lib/client/index.js b/lib/client/index.js index fd416f1..7e97815 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1,5 +1,3 @@ -//var client = ; - var client = module.exports = require('./api'); -client.FileStorage = require('./filestorage'); client.Verifier = require('./verifier'); +client.AirGapped = require('./airgapped'); diff --git a/lib/client/verifier.js b/lib/client/verifier.js index e720074..f45f101 100644 --- a/lib/client/verifier.js +++ b/lib/client/verifier.js @@ -11,16 +11,17 @@ var WalletUtils = require('../walletutils') function Verifier(opts) {}; -Verifier.checkAddress = function(data, address) { - var local = WalletUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); +Verifier.checkAddress = function(credentials, address) { + $.checkState(credentials.isComplete()); + var local = WalletUtils.deriveAddress(credentials.publicKeyRing, address.path, credentials.m, credentials.network); return (local.address == address.address && JSON.stringify(local.publicKeys) == JSON.stringify(address.publicKeys)); }; -Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { - $.checkArgument(walletPrivKey); - var walletPubKey = Bitcore.PrivateKey.fromString(walletPrivKey).toPublicKey().toString(); +Verifier.checkCopayers = function(credentials, copayers) { + $.checkState(credentials.walletPrivKey); + var walletPubKey = Bitcore.PrivateKey.fromString(credentials.walletPrivKey).toPublicKey().toString(); - if (copayers.length != n) { + if (copayers.length != credentials.n) { log.error('Missing public keys in server response'); return false; } @@ -44,8 +45,7 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { if (error) return false; - var myXPubKey = new Bitcore.HDPublicKey(myXPrivKey).toString(); - if (!_.contains(_.pluck(copayers, 'xPubKey'), myXPubKey)) { + if (!_.contains(_.pluck(copayers, 'xPubKey'), credentials.xPubKey)) { log.error('Server response does not contains our public keys') return false; } @@ -53,10 +53,11 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { }; -Verifier.checkTxProposal = function(data, txp) { +Verifier.checkTxProposal = function(credentials, txp) { $.checkArgument(txp.creatorId); + $.checkState(credentials.isComplete()); - var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) { + var creatorXPubKey = _.find(credentials.publicKeyRing, function(xPubKey) { if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true; }); @@ -71,7 +72,7 @@ Verifier.checkTxProposal = function(data, txp) { if (!WalletUtils.verifyMessage(hash, txp.proposalSignature, creatorSigningPubKey)) return false; - return Verifier.checkAddress(data, txp.changeAddress); + return Verifier.checkAddress(credentials, txp.changeAddress); }; module.exports = Verifier; diff --git a/lib/expressapp.js b/lib/expressapp.js index e1c4a63..726b1e6 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -24,7 +24,6 @@ var ExpressApp = function() {}; ExpressApp.start = function(opts) { opts = opts || {}; - WalletService.initialize(opts.WalletService); var app = express(); app.use(function(req, res, next) { @@ -104,13 +103,10 @@ ExpressApp.start = function(opts) { code: 'NOTAUTHORIZED' }), res, req); - var readOnly = req.method == 'GET'; - var auth = { copayerId: credentials.copayerId, message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body), signature: credentials.signature, - readOnly: readOnly, }; WalletService.getInstanceWithAuth(auth, function(err, server) { if (err) return returnError(err, res, req); diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 3c3f54f..04a84cb 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -11,9 +11,6 @@ var AddressManager = require('./addressmanager'); var Utils = require('../walletutils'); -var RO_SIGNING_PATH = "m/1/0"; -var RW_SIGNING_PATH = "m/1/1"; - function Copayer() { this.version = '1.0.0'; }; @@ -31,8 +28,7 @@ Copayer.create = function(opts) { x.id = Utils.xPubToCopayerId(x.xPubKey); x.name = opts.name; x.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently - x.roPubKey = x.getROPubKey(); - x.rwPubKey = x.getRWPubKey(); + x.requestPubKey = x.getRequestPubKey(); x.addressManager = AddressManager.create({ copayerIndex: opts.copayerIndex }); @@ -48,8 +44,7 @@ Copayer.fromObj = function(obj) { x.name = obj.name; x.xPubKey = obj.xPubKey; x.xPubKeySignature = obj.xPubKeySignature; - x.roPubKey = obj.roPubKey; - x.rwPubKey = obj.rwPubKey; + x.requestPubKey = obj.requestPubKey; x.addressManager = AddressManager.fromObj(obj.addressManager); return x; @@ -63,12 +58,8 @@ Copayer.prototype.getPublicKey = function(path) { .toString(); }; -Copayer.prototype.getROPubKey = function() { - return this.getPublicKey(RO_SIGNING_PATH); -}; - -Copayer.prototype.getRWPubKey = function() { - return this.getPublicKey(RW_SIGNING_PATH); +Copayer.prototype.getRequestPubKey = function() { + return this.getPublicKey('m/1/1'); }; diff --git a/lib/model/txproposalaction.js b/lib/model/txproposalaction.js index 8e6dd31..03047c9 100644 --- a/lib/model/txproposalaction.js +++ b/lib/model/txproposalaction.js @@ -11,7 +11,7 @@ TxProposalAction.create = function(opts) { x.createdOn = Math.floor(Date.now() / 1000); x.copayerId = opts.copayerId; - x.type = opts.type || (opts.signatures ? 'accept' : 'reject'); + x.type = opts.type; x.signatures = opts.signatures; x.xpub = opts.xpub; x.comment = opts.comment; diff --git a/lib/server.js b/lib/server.js index aeb6905..8b19e00 100644 --- a/lib/server.js +++ b/lib/server.js @@ -66,8 +66,7 @@ WalletService.getInstance = function() { * @param {Object} opts * @param {string} opts.copayerId - The copayer id making the request. * @param {string} opts.message - The contents of the request to be signed. - * @param {string} opts.signature - Signature of message to be verified using the copayer's roPubKey / rwPubKey - * @param {string} opts.readOnly - Signature of message to be verified using the copayer's roPubKey / rwPubKey + * @param {string} opts.signature - Signature of message to be verified using the copayer's requestPubKey */ WalletService.getInstanceWithAuth = function(opts, cb) { @@ -79,8 +78,7 @@ WalletService.getInstanceWithAuth = function(opts, cb) { if (err) return cb(err); if (!copayer) return cb(new ClientError('NOTAUTHORIZED', 'Copayer not found')); - var pubKey = opts.readOnly ? copayer.roPubKey : copayer.rwPubKey; - var isValid = server._verifySignature(opts.message, opts.signature, pubKey); + var isValid = server._verifySignature(opts.message, opts.signature, copayer.requestPubKey); if (!isValid) return cb(new ClientError('NOTAUTHORIZED', 'Invalid signature')); @@ -314,7 +312,7 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); - var isValid = self._verifySignature(opts.message, opts.signature, copayer.rwPubKey); + var isValid = self._verifySignature(opts.message, opts.signature, copayer.requestPubKey); return cb(null, isValid); }); }; @@ -370,8 +368,7 @@ WalletService.prototype._getUtxos = function(cb) { // Get addresses for this wallet self.storage.fetchAddresses(self.walletId, function(err, addresses) { if (err) return cb(err); - if (addresses.length == 0) - return cb(null, []); + if (addresses.length == 0) return cb(null, []); var addressStrs = _.pluck(addresses, 'address'); var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance @@ -500,7 +497,7 @@ WalletService.prototype.createTx = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message); - if (!self._verifySignature(hash, opts.proposalSignature, copayer.rwPubKey)) + if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) return cb(new ClientError('Invalid proposal signature')); var toAddress; diff --git a/lib/storage.js b/lib/storage.js index 4e333f5..badd80b 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -81,8 +81,7 @@ Storage.prototype.storeWalletAndUpdateCopayersLookup = function(wallet, cb) { _.each(wallet.copayers, function(copayer) { var value = { walletId: wallet.id, - roPubKey: copayer.roPubKey, - rwPubKey: copayer.rwPubKey, + requestPubKey: copayer.requestPubKey, }; ops.push({ type: 'put', diff --git a/lib/walletutils.js b/lib/walletutils.js index 70a983e..4958b9d 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -33,33 +33,6 @@ WalletUtils.signMessage = function(text, privKey) { }; -WalletUtils.accessFromData = function(data) { - if (data.xPrivKey) - return 'full'; - - if (data.rwPrivKey) - return 'readwrite'; - - if (data.roPrivKey) - return 'readonly'; - - return 'none'; -}; - -WalletUtils.accessNameToLevel = function(name) { - - if (name === 'full') - return 30; - if (name === 'readwrite') - return 20; - if (name === 'readonly') - return 10; - if (name === 'none') - return 0; - throw new Error('Bad access name:' + name); -}; - - WalletUtils.verifyMessage = function(text, signature, pubKey) { $.checkArgument(text); $.checkArgument(pubKey); @@ -160,38 +133,46 @@ WalletUtils.privateKeyToAESKey = function(privKey) { return Bitcore.crypto.Hash.sha256(pk.toBuffer()).slice(0, 16).toString('base64'); }; -WalletUtils.decryptWallet = function(data, password) { - $.checkArgument(data.enc); - var extraFields = JSON.parse(sjcl.decrypt(password, data.enc)); - delete data.enc; - return _.extend(data, extraFields); -}; +WalletUtils.signTxp = function(txp, xPrivKey) { + var self = this; -WalletUtils.sjclOpts = { - iter: 5000, -}; + //Derive proper key to sign, for each input + var privs = [], + derived = {}; -WalletUtils.encryptWallet = function(data, accessWithoutEncrytion, password) { + var network = new Bitcore.Address(txp.toAddress).network.name; + var xpriv = new Bitcore.HDPrivateKey(xPrivKey, network); - // Fields to encrypt, given the NOPASSWD access level - var fieldsEncryptByLevel = { - none: _.keys(data), - readonly: ['xPrivKey', 'rwPrivKey', 'publicKeyRing' ], - readwrite: ['xPrivKey', ], - full: [], - }; + _.each(txp.inputs, function(i) { + if (!derived[i.path]) { + derived[i.path] = xpriv.derive(i.path).privateKey; + privs.push(derived[i.path]); + } + }); - var fieldsEncrypt = fieldsEncryptByLevel[accessWithoutEncrytion]; - $.checkState(!_.isUndefined(fieldsEncrypt)); + var t = new Bitcore.Transaction(); - var toEncrypt = _.pick(data, fieldsEncrypt); - var enc = sjcl.encrypt(password, JSON.stringify(toEncrypt), WalletUtils.sjclOpts); - - var ret = _.omit(data, fieldsEncrypt); - ret.enc = enc; - return ret; + _.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; }; +WalletUtils.getNetworkFromXPubKey = function(xPubKey) { + return xPubKey.substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; +}; module.exports = WalletUtils; diff --git a/test/integration/clientApi.js b/test/integration/client.js similarity index 53% rename from test/integration/clientApi.js rename to test/integration/client.js index 3a66e37..0f4adac 100644 --- a/test/integration/clientApi.js +++ b/test/integration/client.js @@ -1,6 +1,7 @@ 'use strict'; var _ = require('lodash'); +var $ = require('preconditions').singleton(); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); @@ -9,7 +10,7 @@ var memdown = require('memdown'); var async = require('async'); var request = require('supertest'); var Client = require('../../lib/client'); -var API = Client.API; +var AirGapped = Client.AirGapped; var Bitcore = require('bitcore'); var WalletUtils = require('../../lib/walletutils'); var ExpressApp = require('../../lib/expressapp'); @@ -20,6 +21,7 @@ var TestData = require('../testdata'); var helpers = {}; helpers.getRequest = function(app) { + $.checkArgument(app); return function(args, cb) { var req = request(app); var r = req[args.method](args.relUrl); @@ -38,51 +40,58 @@ helpers.getRequest = function(app) { }; }; +helpers.newClient = function(app) { + $.checkArgument(app); + return new Client({ + request: helpers.getRequest(app), + }); +}; + helpers.createAndJoinWallet = function(clients, m, n, cb) { clients[0].createWallet('wallet name', 'creator', m, n, 'testnet', function(err, secret) { - if (err) return cb(err); - if (n == 1) return cb(); - - should.exist(secret); - async.each(_.range(n - 1), function(i, cb) { - clients[i + 1].joinWallet(secret, 'copayer ' + (i + 1), function(err, result) { + should.not.exist(err); + + if (n > 1) { + should.exist(secret); + } + + async.series([ + + function(next) { + async.each(_.range(1, n), function(i, cb) { + clients[i].joinWallet(secret, 'copayer ' + i, cb); + }, next); + }, + function(next) { + async.each(_.range(n), function(i, cb) { + clients[i].openWallet(cb); + }, next); + }, + ], + function(err) { should.not.exist(err); - return cb(err); - }); - }, function(err) { - if (err) return cb(err); - return cb(null, { - m: m, - n: n, - secret: secret, + return cb({ + m: m, + n: n, + secret: secret, + }); }); - }); }); }; - -var fsmock = {}; -var content = {}; -fsmock.readFile = function(name, enc, cb) { - if (!content || _.isEmpty(content[name])) - return cb('NOTFOUND'); - - return cb(null, content[name]); -}; -fsmock.writeFile = function(name, data, cb) { - content[name] = data; - return cb(); -}; -fsmock.reset = function() { - content = {}; -}; - -fsmock._get = function(name) { - return content[name]; -}; -fsmock._set = function(name, data) { - return content[name] = data; +helpers.tamperResponse = function(clients, method, url, args, tamper, cb) { + clients = [].concat(clients); + // Use first client to get a clean response from server + clients[0]._doRequest(method, url, args, function(err, result) { + should.not.exist(err); + tamper(result); + // Return tampered data for every client in the list + _.each(clients, function(client) { + client._doRequest = sinon.stub().withArgs(method, url).yields(null, result); + }); + return cb(); + }); }; @@ -133,7 +142,6 @@ describe('client API ', function() { var clients, app; beforeEach(function() { - clients = []; var db = levelup(memdown, { valueEncoding: 'json' }); @@ -148,25 +156,16 @@ describe('client API ', function() { disableLogs: true, }); // Generates 5 clients - _.each(_.range(5), function(i) { - var storage = new Client.FileStorage({ - filename: 'client' + i, - fs: fsmock, - }); - var client = new Client({ - storage: storage, - }); - - client.request = helpers.getRequest(app); - clients.push(client); + clients = _.map(_.range(5), function(i) { + return helpers.newClient(app); }); - fsmock.reset(); blockExplorerMock.reset(); }); describe('Server internals', function() { it('should allow cors', function(done) { - clients[0]._doRequest('options', '/', null, {}, function(err, x, headers) { + clients[0].credentials = {}; + clients[0]._doRequest('options', '/', {}, function(err, x, headers) { headers['access-control-allow-origin'].should.equal('*'); should.exist(headers['access-control-allow-methods']); should.exist(headers['access-control-allow-headers']); @@ -187,10 +186,8 @@ describe('client API ', function() { }); var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); - var client = new Client({ - storage: s2, - }); - client.request = helpers.getRequest(app); + var client = helpers.newClient(app); + client.storage = s2; client.createWallet('1', '2', 1, 1, 'testnet', function(err) { err.code.should.equal('ERROR'); @@ -214,10 +211,8 @@ describe('client API ', function() { }); var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); - var client = new Client({ - storage: s2, - }); - client.request = helpers.getRequest(app); + var client = helpers.newClient(app); + client.storage = s2; client.createWallet('1', '2', 1, 1, 'testnet', function(err) { err.code.should.equal('ERROR'); @@ -226,115 +221,9 @@ describe('client API ', function() { }); }); - describe('Storage Encryption', function() { - beforeEach(function() { - _.each(_.range(3), function(i) { - clients[i].on('needPassword', function(cb) { - return cb('1234#$@#%F,./.**'); - }); - clients[i].on('needNewPassword', 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); - - // 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('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) {}); - }); - describe('Wallet Creation', function() { it('should check balance in a 1-1 ', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].getBalance(function(err, x) { should.not.exist(err); done(); @@ -342,8 +231,7 @@ describe('client API ', function() { }); }); it('should be able to complete wallets in copayer that joined later', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 2, 3, function() { clients[0].getBalance(function(err, x) { should.not.exist(err); clients[1].getBalance(function(err, x) { @@ -358,8 +246,7 @@ describe('client API ', function() { }); it('should not allow to join a full wallet ', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 2, 2, function(w) { should.exist(w.secret); clients[4].joinWallet(w.secret, 'copayer', function(err, result) { err.code.should.contain('WFULL'); @@ -386,362 +273,67 @@ describe('client API ', function() { done(); }); }); - it('should reject wallets with bad signatures', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/wallets/'; - clients[0]._doGetRequest(url, data, function(err, x) { - - // Tamper data - x.wallet.copayers[0].xPubKey = x.wallet.copayers[1].xPubKey; - // Tamper response - clients[1]._doGetRequest = sinon.stub().yields(null, x); - - clients[1].getBalance(function(err, x) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + it('should reject wallets with bad signatures', function(done) { + // Do not complete clients[1] pkr + var openWalletStub = sinon.stub(clients[1], 'openWallet').yields(); + + helpers.createAndJoinWallet(clients, 2, 3, function() { + helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) { + status.wallet.copayers[0].xPubKey = status.wallet.copayers[1].xPubKey; + }, function() { + openWalletStub.restore(); + clients[1].openWallet(function(err, x) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); }); it('should reject wallets with missing signatures', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); - - // Get right response - var data = clients[0]._load({}, function(err, data) { - var url = '/v1/wallets/'; - clients[0]._doGetRequest(url, data, function(err, x) { - - // Tamper data - delete x.wallet.copayers[1].xPubKey; - - // Tamper response - clients[1]._doGetRequest = sinon.stub().yields(null, x); - - clients[1].getBalance(function(err, x) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - - - it('should reject wallets missing caller"s pubkey', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); - - // Get right response - var data = clients[0]._load({}, function(err, data) { - var url = '/v1/wallets/'; - clients[0]._doGetRequest(url, data, function(err, x) { - - // Tamper data. Replace caller's pubkey - x.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey; - // Add a correct signature - x.wallet.copayers[1].xPubKeySignature = WalletUtils.signMessage( - x.wallet.copayers[1].xPubKey, data.walletPrivKey), - - // Tamper response - clients[1]._doGetRequest = sinon.stub().yields(null, x); - - clients[1].getBalance(function(err, x) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - }); - - 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); - - var data = JSON.parse(fsmock._get('client0')); - delete data.rwPrivKey; - fsmock._set('client0', JSON.stringify(data)); - data.rwPrivKey = null; - - // Overwrite client's API auth checks - clients[0]._processWcdAfterRead = function(rawData, xx, cb) { - return cb(null, rawData); - }; - - clients[0].createAddress(function(err, x0) { - err.code.should.equal('NOTAUTHORIZED'); - done(); - }); - }); - }); - 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); - - // Overwrite client's API auth checks - clients[1]._processWcdAfterRead = function(rawData, xx, cb) { - return cb(null, rawData); - }; - - 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); - - // Overwrite client's API auth checks - clients[1]._processWcdAfterRead = function(rawData, xx, cb) { - return cb(null, rawData); - }; - - 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('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); - clients[0].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].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - clients[0].parseTxProposals({ - txps: rawTxps - }, function(err, txs2) { - should.not.exist(err); - txs[0].should.deep.equal(txs2[0]); - done(); - }); - - }); - }); - }); - }); - }); - it('should detect fakes from Tx proposals file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { - should.not.exist(err); - clients[0].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].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - //Tamper - rawTxps[0].amount++; - - clients[0].parseTxProposals({ - txps: rawTxps - }, function(err, txs2) { - err.code.should.equal('SERVERCOMPROMISED'); - done(); - }); - - }); - }); - }); - }); - }); - - it('should create from proxy from airgapped', function(done) { - - var airgapped = clients[0]; - var proxy = clients[1]; - - airgapped.generateKey('testnet', function(err) { - should.not.exist(err); - airgapped.export({ - access: 'readwrite' - }, function(err, str) { - proxy.import(str, function(err) { - should.not.exist(err); - proxy.createWallet('1', '2', 1, 1, 'testnet', - 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(); - }); + // Do not complete clients[1] pkr + var openWalletStub = sinon.stub(clients[1], 'openWallet').yields(); + + helpers.createAndJoinWallet(clients, 2, 3, function() { + helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) { + delete status.wallet.copayers[1].xPubKey; + }, function() { + openWalletStub.restore(); + clients[1].openWallet(function(err, x) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); }); - it('should join from proxy from airgapped', function(done) { - - var airgapped = clients[0]; - var proxy = clients[1]; - var other = clients[2]; // Other copayer - - airgapped.generateKey('testnet', function(err) { - should.not.exist(err); - airgapped.export({ - access: 'readwrite' - }, function(err, str) { - proxy.import(str, function(err) { - should.not.exist(err); - - other.createWallet('1', '2', 1, 2, 'testnet', function(err, secret) { - should.not.exist(err); - proxy.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 reject wallets missing callers pubkey', function(done) { + // Do not complete clients[1] pkr + var openWalletStub = sinon.stub(clients[1], 'openWallet').yields(); + + helpers.createAndJoinWallet(clients, 2, 3, function() { + helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) { + // Replace caller's pubkey + status.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey; + // Add a correct signature + status.wallet.copayers[1].xPubKeySignature = WalletUtils.signMessage(status.wallet.copayers[1].xPubKey, clients[0].credentials.walletPrivKey); + }, function() { + openWalletStub.restore(); + clients[1].openWallet(function(err, x) { + err.code.should.contain('SERVERCOMPROMISED'); + 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('accepted'); - done(); - }); - }); - }); - }); - }); - }); + it.skip('should return wallet status even if wallet is not yet complete', function(done) {}); }); describe('Address Creation', function() { it('should be able to create address in all copayers in a 2-3 wallet', function(done) { this.timeout(5000); - helpers.createAndJoinWallet(clients, 2, 3, function(err) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 2, 3, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -758,8 +350,8 @@ describe('client API ', function() { }); }); it('should see balance on address created by others', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { - should.not.exist(err); + this.timeout(5000); + helpers.createAndJoinWallet(clients, 2, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -779,93 +371,28 @@ describe('client API ', function() { }); }); it('should detect fake addresses', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/addresses/'; - clients[0]._doPostRequest(url, {}, data, function(err, address) { - - // Tamper data - address.address = '2N86pNEpREGpwZyHVC5vrNUCbF9nM1Geh4K'; - - // Tamper response - clients[1]._doPostRequest = sinon.stub().yields(null, address); - - // Grab real response - clients[1].createAddress(function(err, x0) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - it('should detect fake public keys', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/addresses/'; - clients[0]._doPostRequest(url, {}, data, function(err, address) { - - // Tamper data - address.publicKeys = ['0322defe0c3eb9fcd8bc01878e6dbca7a6846880908d214b50a752445040cc5c54', - '02bf3aadc17131ca8144829fa1883c1ac0a8839067af4bca47a90ccae63d0d8037' - ]; - - // Tamper response - clients[1]._doPostRequest = sinon.stub().yields(null, address); - - // Grab real response - clients[1].createAddress(function(err, x0) { - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); - }); - }); - }); - }); - }); - - describe('Wallet Backups and Mobility', function() { - - it('round trip #import #export', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { - should.not.exist(err); - clients[1].export({}, function(err, str) { - should.not.exist(err); - 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')); - delete original.walletPrivKey; // no need to persist it. - clone.should.deep.equal(original); + helpers.createAndJoinWallet(clients, 1, 1, function() { + helpers.tamperResponse(clients[0], 'post', '/v1/addresses/', {}, function(address) { + address.address = '2N86pNEpREGpwZyHVC5vrNUCbF9nM1Geh4K'; + }, function() { + clients[0].createAddress(function(err, x0) { + err.code.should.contain('SERVERCOMPROMISED'); done(); }); - }); }); }); - it('should recreate a wallet, create addresses and receive money', function(done) { - 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) { - should.not.exist(err); - + it('should detect fake public keys', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { + helpers.tamperResponse(clients[0], 'post', '/v1/addresses/', {}, function(address) { + address.publicKeys = [ + '0322defe0c3eb9fcd8bc01878e6dbca7a6846880908d214b50a752445040cc5c54', + '02bf3aadc17131ca8144829fa1883c1ac0a8839067af4bca47a90ccae63d0d8037' + ]; + }, function() { clients[0].createAddress(function(err, x0) { - should.not.exist(err); - should.exist(x0.address); - blockExplorerMock.setUtxo(x0, 10, 2); - clients[0].getBalance(function(err, bal0) { - should.not.exist(err); - bal0.totalAmount.should.equal(10 * 1e8); - bal0.lockedAmount.should.equal(0); - done(); - }); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); @@ -874,7 +401,7 @@ describe('client API ', function() { describe('Transaction Proposals Creation and Locked funds', function() { it('Should lock and release funds through rejection', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -905,7 +432,7 @@ describe('client API ', function() { }); }); it('Should lock and release funds through removal', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 2, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -936,7 +463,7 @@ describe('client API ', function() { }); }); it('Should keep message and refusal texts', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -962,7 +489,7 @@ describe('client API ', function() { }); }); it('Should encrypt proposal message', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -982,7 +509,7 @@ describe('client API ', function() { }); }); it('Should encrypt proposal refusal comment', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1004,9 +531,7 @@ describe('client API ', function() { }); }); it('should detect fake tx proposals (wrong signature)', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1018,34 +543,22 @@ describe('client API ', function() { clients[0].sendTxProposal(opts, function(err, x) { should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/txproposals/'; - clients[0]._doGetRequest(url, data, function(err, txps) { - - // Tamper data - txps[0].proposalSignature = '304402206e4a1db06e00068582d3be41cfc795dcf702451c132581e661e7241ef34ca19202203e17598b4764913309897d56446b51bc1dcd41a25d90fdb5f87a6b58fe3a6920'; - - // Tamper response - clients[0]._doGetRequest = sinon.stub().yields(null, txps); - - // Grab real response - clients[0].getTxProposals({}, function(err, txps) { - should.exist(err); - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + helpers.tamperResponse(clients[0], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].proposalSignature = '304402206e4a1db06e00068582d3be41cfc795dcf702451c132581e661e7241ef34ca19202203e17598b4764913309897d56446b51bc1dcd41a25d90fdb5f87a6b58fe3a6920'; + }, function() { + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); }); }); }); - it('should detect fake tx proposals (tampered amount)', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); + it('should detect fake tx proposals (tampered amount)', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1057,24 +570,13 @@ describe('client API ', function() { clients[0].sendTxProposal(opts, function(err, x) { should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/txproposals/'; - clients[0]._doGetRequest(url, data, function(err, txps) { - - // Tamper data - txps[0].amount = 100000; - - // Tamper response - clients[0]._doGetRequest = sinon.stub().yields(null, txps); - - // Grab real response - clients[0].getTxProposals({}, function(err, txps) { - should.exist(err); - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + helpers.tamperResponse(clients[0], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].amount = 100000; + }, function() { + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); @@ -1082,9 +584,7 @@ describe('client API ', function() { }); }); it('should detect fake tx proposals (change address not it wallet)', function(done) { - helpers.createAndJoinWallet(clients, 2, 2, function(err) { - should.not.exist(err); - + helpers.createAndJoinWallet(clients, 1, 1, function() { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 10, 2); @@ -1096,23 +596,13 @@ describe('client API ', function() { clients[0].sendTxProposal(opts, function(err, x) { should.not.exist(err); - - // Get right response - clients[0]._load({}, function(err, data) { - var url = '/v1/txproposals/'; - clients[0]._doGetRequest(url, data, function(err, txps) { - // Tamper data - txps[0].changeAddress.address = 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5'; - - // Tamper response - clients[0]._doGetRequest = sinon.stub().yields(null, txps); - - // Grab real response - clients[0].getTxProposals({}, function(err, txps) { - should.exist(err); - err.code.should.contain('SERVERCOMPROMISED'); - done(); - }); + helpers.tamperResponse(clients[0], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].changeAddress.address = 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5'; + }, function() { + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); }); }); }); @@ -1120,8 +610,7 @@ describe('client API ', function() { }); }); it('Should return only main addresses (case 1)', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); blockExplorerMock.setUtxo(x0, 1, 1); @@ -1142,8 +631,7 @@ describe('client API ', function() { }); }); it('Should return only main addresses (case 2)', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { - should.not.exist(err); + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); clients[0].createAddress(function(err, x0) { @@ -1164,7 +652,7 @@ describe('client API ', function() { describe('Transactions Signatures and Rejection', function() { this.timeout(5000); it('Send and broadcast in 1-1 wallet', function(done) { - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1194,7 +682,7 @@ describe('client API ', function() { }); }); it('Send and broadcast in 2-3 wallet', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1240,7 +728,7 @@ describe('client API ', function() { }); it('Send, reject, 2 signs and broadcast in 2-3 wallet', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1277,7 +765,7 @@ describe('client API ', function() { }); it('Send, reject in 3-4 wallet', function(done) { - helpers.createAndJoinWallet(clients, 3, 4, function(err, w) { + helpers.createAndJoinWallet(clients, 3, 4, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1312,7 +800,7 @@ describe('client API ', function() { }); it('Should not allow to reject or sign twice', function(done) { - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + helpers.createAndJoinWallet(clients, 2, 3, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1352,7 +840,7 @@ describe('client API ', function() { describe('Transaction history', function() { it('should get transaction history', function(done) { blockExplorerMock.setHistory(TestData.history); - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); should.exist(x0.address); @@ -1367,7 +855,7 @@ describe('client API ', function() { }); it('should get empty transaction history when there are no addresses', function(done) { blockExplorerMock.setHistory(TestData.history); - helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + helpers.createAndJoinWallet(clients, 1, 1, function(w) { clients[0].getTxHistory({}, function(err, txs) { should.not.exist(err); should.exist(txs); @@ -1379,4 +867,217 @@ describe('client API ', function() { it.skip('should get transaction history decorated with proposal', function(done) {}); it.skip('should get paginated transaction history', function(done) {}); }); + + describe('Mobility, backup & recovery', function() { + describe('Export & Import', function() { + describe('Success', function() { + var address, importedClient; + beforeEach(function(done) { + importedClient = null; + helpers.createAndJoinWallet(clients, 1, 1, function() { + clients[0].createAddress(function(err, addr) { + should.not.exist(err); + should.exist(addr.address); + address = addr.address; + done(); + }); + }); + }); + afterEach(function(done) { + importedClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list.length.should.equal(1); + list[0].address.should.equal(address); + done(); + }); + }); + + it('should export & import', function() { + var exported = clients[0].export(); + + importedClient = helpers.newClient(app); + importedClient.import(exported); + }); + it.skip('should export & import compressed', function() { + var walletId = clients[0].credentials.walletId; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; + + var exported = clients[0].export({ + compressed: true + }); + + importedClient = helpers.newClient(app); + importedClient.import(exported, { + compressed: true + }); + importedClient.credentials.walletId.should.equal(walletId); + importedClient.credentials.walletName.should.equal(walletName); + importedClient.credentials.copayerName.should.equal(copayerName); + }); + it('should export & import encrypted', function() { + var xPrivKey = clients[0].credentials.xPrivKey; + should.exist(xPrivKey); + + var exported = clients[0].export({ + password: '123' + }); + exported.should.not.contain(xPrivKey); + + importedClient = helpers.newClient(app); + importedClient.import(exported, { + password: '123' + }); + should.exist(importedClient.credentials.xPrivKey); + importedClient.credentials.xPrivKey.should.equal(xPrivKey); + }); + it('should export & import compressed & encrypted', function() { + var exported = clients[0].export({ + compressed: true, + password: '123' + }); + + importedClient = helpers.newClient(app); + importedClient.import(exported, { + compressed: true, + password: '123' + }); + }); + }); + describe('Fail', function() { + it.skip('should fail to export compressed & import uncompressed', function() {}); + it.skip('should fail to export uncompressed & import compressed', function() {}); + it.skip('should fail to export unencrypted & import with password', function() {}); + it.skip('should fail to export encrypted & import with incorrect password', function() {}); + }); + }); + + describe('Recovery', function() { + it('should be able to regain access to a 1-1 wallet with just the xPriv', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { + var xpriv = clients[0].credentials.xPrivKey; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; + + clients[0].createAddress(function(err, addr) { + should.not.exist(err); + should.exist(addr); + + var recoveryClient = helpers.newClient(app); + recoveryClient.seedFromExtendedPrivateKey(xpriv); + recoveryClient.openWallet(function(err) { + console.log(err); + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); + }); + }); + }); + }); + }); + }); + }); + + describe('Air gapped related flows', function() { + it('should create wallet in proxy from airgapped', function(done) { + var airgapped = new AirGapped({ + network: 'testnet' + }); + var seed = airgapped.getSeed(); + + var proxy = helpers.newClient(app); + proxy.seedFromAirGapped(seed); + should.not.exist(proxy.credentials.xPrivKey); + proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { + should.not.exist(err); + proxy.getStatus(function(err, status) { + should.not.exist(err); + status.wallet.name.should.equal('wallet name'); + done(); + }); + }); + }); + + it('should be able to sign from airgapped client and broadcast from proxy', function(done) { + var airgapped = new AirGapped({ + network: 'testnet' + }); + var seed = airgapped.getSeed(); + + var proxy = helpers.newClient(app); + proxy.seedFromAirGapped(seed); + + async.waterfall([ + + function(next) { + proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { + should.not.exist(err); + proxy.createAddress(function(err, address) { + should.not.exist(err); + should.exist(address.address); + blockExplorerMock.setUtxo(address, 1, 1); + var opts = { + amount: 1200000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hello 1-1', + }; + proxy.sendTxProposal(opts, next); + }); + }); + }, + function(txp, next) { + should.exist(txp); + proxy.signTxProposal(txp, function(err, txp) { + should.exist(err); + should.not.exist(txp); + err.message.should.equal('You do not have the required keys to sign transactions'); + next(null, txp); + }); + }, + function(txp, next) { + proxy.getTxProposals({ + forAirGapped: true + }, next); + }, + function(bundle, next) { + var signatures = airgapped.signTxProposal(bundle.txps[0], bundle.publicKeyRing, bundle.m, bundle.n); + next(null, signatures); + }, + function(signatures, next) { + proxy.getTxProposals({}, function(err, txps) { + should.not.exist(err); + var txp = txps[0]; + txp.signatures = signatures; + async.each(txps, function(txp, cb) { + proxy.signTxProposal(txp, function(err, txp) { + should.not.exist(err); + proxy.broadcastTxProposal(txp, function(err, txp) { + should.not.exist(err); + txp.status.should.equal('broadcasted'); + should.exist(txp.txid); + cb(); + }); + }); + }, function(err) { + next(err); + }); + }); + }, + ], + function(err) { + should.not.exist(err); + done(); + } + ); + }); + it.skip('should be able to detect tampered PKR when signing on airgapped client', function(done) {}); + it.skip('should be able to detect tampered proposal when signing on airgapped client', function(done) {}); + it.skip('should be able to detect tampered change address when signing on airgapped client', function(done) {}); + }); });