Browse Source

Merge pull request #74 from matiu/airgapped03

Air gapped
activeAddress
Ivan Socolsky 10 years ago
parent
commit
e67347d8cc
  1. 28
      README.md
  2. 2
      bit-wallet/bit
  3. 20
      bit-wallet/bit-genkey
  4. 11
      bit-wallet/bit-txproposals
  5. 394
      lib/client/api.js
  6. 2
      lib/client/verifier.js
  7. 4
      lib/clienterror.js
  8. 4
      lib/expressapp.js
  9. 5
      lib/walletutils.js
  10. 175
      test/integration/clientApi.js

28
README.md

@ -91,57 +91,57 @@ bit import output.dat
bit recreate bit recreate
``` ```
# Airgapped Operation (TODO) # Airgapped Operation
### On the Air-gapped device ### On the Air-gapped device
``` ```
git genkey bit genkey
git export -o wallet.dat --readonly (or --nosigning) bit export -o wallet.dat --readonly (or --nosigning)
``` ```
### Proxy machine ### Proxy machine
``` ```
git join secret -i wallet.dat bit join secret -i wallet.dat
git balance bit balance
# Export pending transaction to be signed offline # Export pending transaction to be signed offline
git txproposals -o txproposals.dat bit txproposals -o txproposals.dat
``` ```
## Back to air-gapped device ## Back to air-gapped device
### To check tx proposals: ### To check tx proposals:
``` ```
git txproposals -i txproposals.dat bit txproposals -i txproposals.dat
``` ```
First time txproposals is running on the air gapped devices, the public keys of the copayers will be imported from the txproposals archive. That information is exported automatically by the proxy machine, and encrypted copayer's xpriv derivatives. First time txproposals is running on the air gapped devices, the public keys of the copayers will be imported from the txproposals archive. That information is exported automatically by the proxy machine, and encrypted copayer's xpriv derivatives.
### Sign them ### Sign them
``` ```
git sign -i txproposals.dat -o txproposals-signed.dat bit sign -i txproposals.dat -o txproposals-signed.dat
# Or With filter # Or With filter
git sign e01e -i txproposals.dat -o txproposals-signed.dat bit sign e01e -i txproposals.dat -o txproposals-signed.dat
``` ```
## Back to proxy machine ## Back to proxy machine
``` ```
git sign -i txproposals-signed.dat bit sign -i txproposals-signed.dat
``` ```
# Password protection (TODO) # Password protection (TODO)
### encrypts everything by default ### encrypts everything by default
``` ```
git create myWallet 2-3 -p password bit create myWallet 2-3 -p password
# Or (interactive mode) # Or (interactive mode)
git create myWallet 2-3 -p bit create myWallet 2-3 -p
Enter password: Enter password:
``` ```
### allows readonly operations without password (encrypts xpriv, and leave readonlySigningKey unencrypted) ### allows readonly operations without password (encrypts xpriv, and leave readonlySigningKey unencrypted)
``` ```
git create myWallet 2-3 -p --nopasswd:ro bit create myWallet 2-3 -p --nopasswd:ro
``` ```
### allows readwrite operations without password (only encrypts xpriv) ### allows readwrite operations without password (only encrypts xpriv)
``` ```
git create myWallet 2-3 -p --nopasswd:rw bit create myWallet 2-3 -p --nopasswd:rw
``` ```
# Local data # Local data

2
bit-wallet/bit

@ -19,6 +19,8 @@ program
.command('import', 'import wallet critical data') .command('import', 'import wallet critical data')
.command('confirm', 'show copayer\'s data for confirmation') .command('confirm', 'show copayer\'s data for confirmation')
.command('recreate', 'recreate a wallet on a remove server given local infomation') .command('recreate', 'recreate a wallet on a remove server given local infomation')
.command('txproposals', 'list transactions proposals')
.command('genkey', 'generates extended private key for later wallet usage')
.parse(process.argv); .parse(process.argv);

20
bit-wallet/bit-genkey

@ -0,0 +1,20 @@
#!/usr/bin/env node
var _ = require('lodash');
var program = require('commander');
var Client = require('../lib/client');
var utils = require('./cli-utils');
program = utils.configureCommander(program);
program
.option('-t, --testnet', 'Create a Testnet Extended Private Key')
.parse(process.argv);
var args = program.args;
var client = utils.getClient(program);
var network = program.testnet ? 'testnet' : 'livenet';
client.generateKey(network, function(err) {
utils.die(err);
console.log(' * ' + _.capitalize(network) + ' Extended Private Key Created.');
});

11
bit-wallet/bit-txproposals

@ -23,8 +23,15 @@ function end(err, txps, rawtxps) {
} }
utils.renderTxProposals(txps); utils.renderTxProposals(txps);
if (program.output) { if (program.output) {
fs.writeFileSync(program.output, JSON.stringify(rawtxps));
console.log(' * Proposals Saved to: %s\n', 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);
});
} }
}; };

394
lib/client/api.js

@ -11,13 +11,16 @@ log.debug = log.verbose;
var Bitcore = require('bitcore') var Bitcore = require('bitcore')
var WalletUtils = require('../walletutils'); var WalletUtils = require('../walletutils');
var Verifier = require('./verifier'); var Verifier = require('./verifier');
var ServerCompromisedError = require('./servercompromisederror') var ServerCompromisedError = require('./servercompromisederror');
var ClientError = require('../clienterror');
var BASE_URL = 'http://localhost:3001/copay/api'; var BASE_URL = 'http://localhost:3001/copay/api';
var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'publicKeyRing', 'sharedEncryptingKey']; var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'n', 'publicKeyRing', 'sharedEncryptingKey'];
var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey'];
var WALLET_AIRGAPPED_TOCOMPLETE = ['publicKeyRing', 'm', 'n', 'sharedEncryptingKey'];
function _encryptMessage(message, encryptingKey) { function _encryptMessage(message, encryptingKey) {
if (!message) return null; if (!message) return null;
return WalletUtils.encryptMessage(message, encryptingKey); return WalletUtils.encryptMessage(message, encryptingKey);
@ -52,13 +55,17 @@ function _parseError(body) {
}; };
} }
} }
var code = body.code || 'ERROR'; var ret;
var message = body.error || 'There was an unknown error processing the request'; if (body.code) {
log.error(code, message); ret = new ClientError(body.code, body.message);
return { } else {
message: message, ret = {
code: code code: 'ERROR',
}; error: body.error || 'There was an unknown error processing the request',
};
}
log.error(ret);
return ret;
}; };
function _signRequest(method, url, args, privKey) { function _signRequest(method, url, args, privKey) {
@ -66,6 +73,34 @@ function _signRequest(method, url, args, privKey) {
return WalletUtils.signMessage(message, 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) { function API(opts) {
if (!opts.storage) { if (!opts.storage) {
@ -83,73 +118,106 @@ function API(opts) {
} }
}; };
API.prototype._tryToCompleteFromServer = function(wcd, cb) {
API.prototype._tryToComplete = function(data, cb) { if (!wcd.walletPrivKey)
var self = this; return cb('Could not perform that action. Wallet Incomplete');
var self = this;
var url = '/v1/wallets/'; var url = '/v1/wallets/';
self._doGetRequest(url, data, function(err, ret) { self._doGetRequest(url, wcd, function(err, ret) {
if (err) return cb(err); if (err) return cb(err);
var wallet = ret.wallet; var wallet = ret.wallet;
if (wallet.status != 'complete') if (wallet.status != 'complete')
return cb('Wallet Incomplete'); return cb('Wallet Incomplete');
if (!Verifier.checkCopayers(wallet.copayers, data.walletPrivKey, if (!Verifier.checkCopayers(wallet.copayers, wcd.walletPrivKey,
data.xPrivKey, data.n)) { wcd.xPrivKey, wcd.n)) {
return cb(new ServerCompromisedError( return cb(new ServerCompromisedError(
'Copayers in the wallet could not be verified to have known the wallet secret')); 'Copayers in the wallet could not be verified to have known the wallet secret'));
} }
data.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') wcd.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey')
self.storage.save(data, function(err) { self.storage.save(wcd, function(err) {
return cb(err, data); 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.storage.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._load = function(cb) { API.prototype._load = function(cb) {
var self = this; var self = this;
this.storage.load(function(err, data) { this.storage.load(function(err, wcd) {
if (err || !data) { if (err || !wcd) {
return cb(err || 'Wallet file not found.'); return cb(err || 'wcd file not found.');
} }
return cb(null, data); return cb(null, wcd);
}); });
}; };
API.prototype._loadAndCheck = function(cb) { /**
* _loadAndCheck
*
* @param opts.pkr
*/
API.prototype._loadAndCheck = function(opts, cb) {
var self = this; var self = this;
this._load(function(err, data) { this._load(function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
if (data.n > 1) {
var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n;
if (!pkrComplete) { if (!wcd.n || (wcd.n > 1 && wcd.publicKeyRing.length != wcd.n)) {
return self._tryToComplete(data, cb); return self._tryToComplete(opts, wcd, cb);
}
} }
return cb(null, data);
return cb(null, wcd);
}); });
}; };
API.prototype._doRequest = function(method, url, args, data, cb) { API.prototype._doRequest = function(method, url, args, wcd, cb) {
var reqSignature; var reqSignature;
data = data || {}; wcd = wcd || {};
if (method == 'get') { if (method == 'get') {
if (data.roPrivKey) if (wcd.roPrivKey)
reqSignature = _signRequest(method, url, args, data.roPrivKey); reqSignature = _signRequest(method, url, args, wcd.roPrivKey);
} else { } else {
if (data.rwPrivKey) if (wcd.rwPrivKey)
reqSignature = _signRequest(method, url, args, data.rwPrivKey); reqSignature = _signRequest(method, url, args, wcd.rwPrivKey);
} }
var absUrl = this.baseUrl + url; var absUrl = this.baseUrl + url;
@ -157,7 +225,7 @@ API.prototype._doRequest = function(method, url, args, data, cb) {
// relUrl: only for testing with `supertest` // relUrl: only for testing with `supertest`
relUrl: this.basePath + url, relUrl: this.basePath + url,
headers: { headers: {
'x-identity': data.copayerId, 'x-identity': wcd.copayerId,
'x-signature': reqSignature, 'x-signature': reqSignature,
}, },
method: method, method: method,
@ -183,38 +251,15 @@ API.prototype._doRequest = function(method, url, args, data, cb) {
}; };
API.prototype._doPostRequest = function(url, args, data, cb) { API.prototype._doPostRequest = function(url, args, wcd, cb) {
return this._doRequest('post', url, args, data, cb); return this._doRequest('post', url, args, wcd, cb);
}; };
API.prototype._doGetRequest = function(url, data, cb) { API.prototype._doGetRequest = function(url, wcd, cb) {
return this._doRequest('get', url, {}, data, cb); return this._doRequest('get', url, {}, wcd, cb);
}; };
API.prototype._initData = function(network, walletPrivKey, m, n) {
var xPrivKey = new Bitcore.HDPrivateKey(network);
var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString();
var roPrivKey = xPrivKey.derive('m/1/0').privateKey;
var rwPrivKey = xPrivKey.derive('m/1/1').privateKey;
var sharedEncryptingKey = Bitcore.crypto.Hash.sha256(walletPrivKey.toBuffer()).slice(0, 16).toString('base64');
var copayerId = WalletUtils.xPubToCopayerId(xPubKey);
var data = {
copayerId: copayerId,
xPrivKey: xPrivKey.toString(),
publicKeyRing: [xPubKey],
network: network,
m: m,
n: n,
roPrivKey: roPrivKey.toWIF(),
rwPrivKey: rwPrivKey.toWIF(),
walletPrivKey: walletPrivKey.toWIF(),
sharedEncryptingKey: sharedEncryptingKey,
};
return data;
};
API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) { API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) {
var args = { var args = {
walletId: walletId, walletId: walletId,
@ -229,6 +274,22 @@ API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayer
}); });
}; };
API.prototype.generateKey = function(network, cb) {
var self = this;
network = network || 'livenet';
if (!_.contains(['testnet', 'livenet'], network))
return cb('Invalid network');
this.storage.load(function(err, wcd) {
if (wcd)
return cb(self.storage.getName() + ' already contains a wallet');
var wcd = _initWcd(network);
self.storage.save(wcd, function(err) {
return cb(err, null);
});
});
};
API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) { API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) {
var self = this; var self = this;
@ -236,10 +297,13 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb
if (!_.contains(['testnet', 'livenet'], network)) if (!_.contains(['testnet', 'livenet'], network))
return cb('Invalid network'); return cb('Invalid network');
this.storage.load(function(err, data) { this.storage.load(function(err, wcd) {
if (data) if (wcd && wcd.n)
return cb(self.storage.getName() + ' already contains a wallet'); 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 walletPrivKey = new Bitcore.PrivateKey();
var args = { var args = {
name: walletName, name: walletName,
@ -255,11 +319,14 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb
var walletId = body.walletId; var walletId = body.walletId;
var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); var secret = WalletUtils.toSecret(walletId, walletPrivKey, network);
var data = self._initData(network, walletPrivKey, m, n);
self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[0], copayerName, wcd = wcd || _initWcd(network);
_addWalletToWcd(wcd, walletPrivKey, m, n)
self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[0], copayerName,
function(err, wallet) { function(err, wallet) {
if (err) return cb(err); if (err) return cb(err);
self.storage.save(data, function(err) { self.storage.save(wcd, function(err) {
return cb(err, n > 1 ? secret : null); return cb(err, n > 1 ? secret : null);
}); });
}); });
@ -270,16 +337,16 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb
API.prototype.reCreateWallet = function(walletName, cb) { API.prototype.reCreateWallet = function(walletName, cb) {
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var walletPrivKey = new Bitcore.PrivateKey(); var walletPrivKey = new Bitcore.PrivateKey();
var args = { var args = {
name: walletName, name: walletName,
m: data.m, m: wcd.m,
n: data.n, n: wcd.n,
pubKey: walletPrivKey.toPublicKey().toString(), pubKey: walletPrivKey.toPublicKey().toString(),
network: data.network, network: wcd.network,
}; };
var url = '/v1/wallets/'; var url = '/v1/wallets/';
self._doPostRequest(url, args, {}, function(err, body) { self._doPostRequest(url, args, {}, function(err, body) {
@ -287,11 +354,11 @@ API.prototype.reCreateWallet = function(walletName, cb) {
var walletId = body.walletId; var walletId = body.walletId;
var secret = WalletUtils.toSecret(walletId, walletPrivKey, data.network); var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network);
var i = 0; var i = 0;
async.each(data.publicKeyRing, function(xpub, next) { async.each(wcd.publicKeyRing, function(xpub, next) {
var copayerName = 'recovered Copayer #' + i; var copayerName = 'recovered Copayer #' + i;
self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[i++], copayerName, next); self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next);
}, function(err) { }, function(err) {
return cb(err); return cb(err);
}); });
@ -303,22 +370,22 @@ API.prototype.reCreateWallet = function(walletName, cb) {
API.prototype.joinWallet = function(secret, copayerName, cb) { API.prototype.joinWallet = function(secret, copayerName, cb) {
var self = this; var self = this;
this.storage.load(function(err, data) { this.storage.load(function(err, wcd) {
if (data) if (wcd && wcd.n)
return cb('Storage already contains a wallet'); return cb(self.storage.getName() + ' already contains a wallet');
try { try {
var secretData = WalletUtils.fromSecret(secret); var secretData = WalletUtils.fromSecret(secret);
} catch (ex) { } catch (ex) {
return cb(ex); return cb(ex);
} }
var data = self._initData(secretData.network, secretData.walletPrivKey); wcd = wcd || _initWcd(secretData.network);
self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, data.publicKeyRing[0], copayerName,
function(err, wallet) { self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, wcd.publicKeyRing[0], copayerName,
function(err, joinedWallet) {
if (err) return cb(err); if (err) return cb(err);
data.m = wallet.m; _addWalletToWcd(wcd, secretData.walletPrivKey, joinedWallet.m, joinedWallet.n);
data.n = wallet.n; self.storage.save(wcd, cb);
self.storage.save(data, cb);
}); });
}); });
}; };
@ -326,13 +393,13 @@ API.prototype.joinWallet = function(secret, copayerName, cb) {
API.prototype.getStatus = function(cb) { API.prototype.getStatus = function(cb) {
var self = this; var self = this;
this._load(function(err, data) { this._load(function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/wallets/'; var url = '/v1/wallets/';
self._doGetRequest(url, data, function(err, result) { self._doGetRequest(url, wcd, function(err, result) {
_processTxps(result.pendingTxps, data.sharedEncryptingKey); _processTxps(result.pendingTxps, wcd.sharedEncryptingKey);
return cb(err, result, data.copayerId); return cb(err, result, wcd.copayerId);
}); });
}); });
}; };
@ -351,36 +418,36 @@ API.prototype.sendTxProposal = function(opts, cb) {
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
if (!data.rwPrivKey) if (!wcd.rwPrivKey)
return cb('No key to generate proposals'); return cb('No key to generate proposals');
var args = { var args = {
toAddress: opts.toAddress, toAddress: opts.toAddress,
amount: opts.amount, amount: opts.amount,
message: _encryptMessage(opts.message, data.sharedEncryptingKey), message: _encryptMessage(opts.message, wcd.sharedEncryptingKey),
}; };
var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message);
args.proposalSignature = WalletUtils.signMessage(hash, data.rwPrivKey); args.proposalSignature = WalletUtils.signMessage(hash, wcd.rwPrivKey);
log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature);
var url = '/v1/txproposals/'; var url = '/v1/txproposals/';
self._doPostRequest(url, args, data, cb); self._doPostRequest(url, args, wcd, cb);
}); });
}; };
API.prototype.createAddress = function(cb) { API.prototype.createAddress = function(cb) {
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/addresses/'; var url = '/v1/addresses/';
self._doPostRequest(url, {}, data, function(err, address) { self._doPostRequest(url, {}, wcd, function(err, address) {
if (err) return cb(err); if (err) return cb(err);
if (!Verifier.checkAddress(data, address)) { if (!Verifier.checkAddress(wcd, address)) {
return cb(new ServerCompromisedError('Server sent fake address')); return cb(new ServerCompromisedError('Server sent fake address'));
} }
@ -396,16 +463,16 @@ API.prototype.createAddress = function(cb) {
API.prototype.getMainAddresses = function(opts, cb) { API.prototype.getMainAddresses = function(opts, cb) {
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/addresses/'; var url = '/v1/addresses/';
self._doGetRequest(url, data, function(err, addresses) { self._doGetRequest(url, wcd, function(err, addresses) {
if (err) return cb(err); if (err) return cb(err);
if (!opts.doNotVerify) { if (!opts.doNotVerify) {
var fake = _.any(addresses, function(address) { var fake = _.any(addresses, function(address) {
return !Verifier.checkAddress(data, address); return !Verifier.checkAddress(wcd, address);
}); });
if (fake) if (fake)
return cb(new ServerCompromisedError('Server sent fake address')); return cb(new ServerCompromisedError('Server sent fake address'));
@ -422,15 +489,16 @@ API.prototype.history = function(limit, cb) {
API.prototype.getBalance = function(cb) { API.prototype.getBalance = function(cb) {
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/balance/'; var url = '/v1/balance/';
self._doGetRequest(url, data, cb); self._doGetRequest(url, wcd, cb);
}); });
}; };
/** /**
* export * Export does not try to complete the wallet from the server. Exports the
* wallet as it is now.
* *
* @param opts.access =['full', 'readonly', 'readwrite'] * @param opts.access =['full', 'readonly', 'readwrite']
*/ */
@ -440,11 +508,11 @@ API.prototype.export = function(opts, cb) {
opts = opts || {}; opts = opts || {};
var access = opts.access || 'full'; var access = opts.access || 'full';
this._loadAndCheck(function(err, data) { this._load(function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var v = []; var v = [];
var myXPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString(); var myXPubKey = (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString();
_.each(WALLET_CRITICAL_DATA, function(k) { _.each(WALLET_CRITICAL_DATA, function(k) {
var d; var d;
@ -456,18 +524,18 @@ API.prototype.export = function(opts, cb) {
// Skips own pub key IF priv key is exported // Skips own pub key IF priv key is exported
if (access == 'full' && k === 'publicKeyRing') { if (access == 'full' && k === 'publicKeyRing') {
d = _.without(data[k], myXPubKey); d = _.without(wcd[k], myXPubKey);
} else { } else {
d = data[k]; d = wcd[k];
} }
v.push(d); v.push(d);
}); });
if (access != 'full') { if (access != 'full') {
v.push(data.copayerId); v.push(wcd.copayerId);
v.push(data.roPrivKey); v.push(wcd.roPrivKey);
if (access == 'readwrite') { if (access == 'readwrite') {
v.push(data.rwPrivKey); v.push(wcd.rwPrivKey);
} }
} }
@ -479,36 +547,34 @@ API.prototype.export = function(opts, cb) {
API.prototype.import = function(str, cb) { API.prototype.import = function(str, cb) {
var self = this; var self = this;
this.storage.load(function(err, data) { this.storage.load(function(err, wcd) {
if (data) if (wcd)
return cb('Storage already contains a wallet'); return cb('Storage already contains a wallet');
data = {}; wcd = {};
var inData = JSON.parse(str); var inData = JSON.parse(str);
var i = 0; var i = 0;
_.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) {
data[k] = inData[i++]; wcd[k] = inData[i++];
}); });
if (data.xPrivKey) { if (wcd.xPrivKey) {
var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey); var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey);
var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); var xPubKey = new Bitcore.HDPublicKey(xpriv).toString();
data.publicKeyRing.unshift(xPubKey); wcd.publicKeyRing.unshift(xPubKey);
data.copayerId = WalletUtils.xPubToCopayerId(xPubKey); wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey);
data.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF();
data.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); wcd.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF();
} }
data.n = data.publicKeyRing.length; if (!wcd.publicKeyRing)
return cb('Invalid source wallet');
if (!data.copayerId || !data.n || !data.m)
return cb('Invalid source data');
data.network = data.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet';
self.storage.save(data, function(err) { self.storage.save(wcd, function(err) {
return cb(err, WalletUtils.accessFromData(data)); return cb(err, WalletUtils.accessFromData(wcd));
}); });
}); });
}; };
@ -517,23 +583,19 @@ API.prototype.import = function(str, cb) {
* *
*/ */
API.prototype.parseTxProposals = function(txps, cb) { API.prototype.parseTxProposals = function(txData, cb) {
var self = this; var self = this;
this._load(function(err, data) { this._loadAndCheck({
toComplete: txData.toComplete
}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
if (data.n > 1) {
var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n;
if (!pkrComplete) {
return cb('Wallet Incomplete');
}
}
_processTxps(txps, data.sharedEncryptingKey); var txps = txData.txps;
_processTxps(txps, wcd.sharedEncryptingKey);
var fake = _.any(txps, function(txp) { var fake = _.any(txps, function(txp) {
return (!Verifier.checkTxProposal(data, txp)); return (!Verifier.checkTxProposal(wcd, txp));
}); });
if (fake) if (fake)
@ -555,20 +617,20 @@ API.prototype.parseTxProposals = function(txps, cb) {
API.prototype.getTxProposals = function(opts, cb) { API.prototype.getTxProposals = function(opts, cb) {
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/txproposals/'; var url = '/v1/txproposals/';
self._doGetRequest(url, data, function(err, txps) { self._doGetRequest(url, wcd, function(err, txps) {
if (err) return cb(err); if (err) return cb(err);
var rawTxps; var rawTxps;
if (opts.getRawTxps) if (opts.getRawTxps)
rawTxps = JSON.parse(JSON.stringify(txps)); rawTxps = JSON.parse(JSON.stringify(txps));
_processTxps(txps, data.sharedEncryptingKey); _processTxps(txps, wcd.sharedEncryptingKey);
var fake = _.any(txps, function(txp) { var fake = _.any(txps, function(txp) {
return (!opts.doNotVerify && !Verifier.checkTxProposal(data, txp)); return (!opts.doNotVerify && !Verifier.checkTxProposal(wcd, txp));
}); });
if (fake) if (fake)
@ -579,14 +641,14 @@ API.prototype.getTxProposals = function(opts, cb) {
}); });
}; };
API.prototype._getSignaturesFor = function(txp, data) { API.prototype._getSignaturesFor = function(txp, wcd) {
//Derive proper key to sign, for each input //Derive proper key to sign, for each input
var privs = [], var privs = [],
derived = {}; derived = {};
var network = new Bitcore.Address(txp.toAddress).network.name; var network = new Bitcore.Address(txp.toAddress).network.name;
var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network); var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey, network);
_.each(txp.inputs, function(i) { _.each(txp.inputs, function(i) {
if (!derived[i.path]) { if (!derived[i.path]) {
@ -619,37 +681,49 @@ API.prototype.getSignatures = function(txp, cb) {
$.checkArgument(txp.creatorId); $.checkArgument(txp.creatorId);
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
if (!Verifier.checkTxProposal(data, txp)) { if (!Verifier.checkTxProposal(wcd, txp)) {
return cb(new ServerCompromisedError('Transaction proposal is invalid')); return cb(new ServerCompromisedError('Transaction proposal is invalid'));
} }
return cb(null, self._getSignaturesFor(txp, data)); return cb(null, self._getSignaturesFor(txp, wcd));
}); });
}; };
API.prototype.getEncryptedWalletData = function(cb) {
var self = this;
this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err);
var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE));
return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey)));
});
};
API.prototype.signTxProposal = function(txp, cb) { API.prototype.signTxProposal = function(txp, cb) {
$.checkArgument(txp.creatorId); $.checkArgument(txp.creatorId);
var self = this; var self = this;
this._loadAndCheck(function(err, data) { this._loadAndCheck({}, function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
if (!Verifier.checkTxProposal(data, txp)) { if (!Verifier.checkTxProposal(wcd, txp)) {
return cb(new ServerCompromisedError('Server sent fake transaction proposal')); return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
} }
var signatures = txp.signatures || self._getSignaturesFor(txp, data); var signatures = txp.signatures || self._getSignaturesFor(txp, wcd);
var url = '/v1/txproposals/' + txp.id + '/signatures/'; var url = '/v1/txproposals/' + txp.id + '/signatures/';
var args = { var args = {
signatures: signatures signatures: signatures
}; };
self._doPostRequest(url, args, data, cb); self._doPostRequest(url, args, wcd, cb);
}); });
}; };
@ -658,27 +732,27 @@ API.prototype.rejectTxProposal = function(txp, reason, cb) {
var self = this; var self = this;
this._loadAndCheck( this._loadAndCheck({},
function(err, data) { function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/txproposals/' + txp.id + '/rejections/'; var url = '/v1/txproposals/' + txp.id + '/rejections/';
var args = { var args = {
reason: _encryptMessage(reason, data.sharedEncryptingKey) || '', reason: _encryptMessage(reason, wcd.sharedEncryptingKey) || '',
}; };
self._doPostRequest(url, args, data, cb); self._doPostRequest(url, args, wcd, cb);
}); });
}; };
API.prototype.broadcastTxProposal = function(txp, cb) { API.prototype.broadcastTxProposal = function(txp, cb) {
var self = this; var self = this;
this._loadAndCheck( this._loadAndCheck({},
function(err, data) { function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/txproposals/' + txp.id + '/broadcast/'; var url = '/v1/txproposals/' + txp.id + '/broadcast/';
self._doPostRequest(url, {}, data, cb); self._doPostRequest(url, {}, wcd, cb);
}); });
}; };
@ -686,11 +760,11 @@ API.prototype.broadcastTxProposal = function(txp, cb) {
API.prototype.removeTxProposal = function(txp, cb) { API.prototype.removeTxProposal = function(txp, cb) {
var self = this; var self = this;
this._loadAndCheck( this._loadAndCheck({},
function(err, data) { function(err, wcd) {
if (err) return cb(err); if (err) return cb(err);
var url = '/v1/txproposals/' + txp.id; var url = '/v1/txproposals/' + txp.id;
self._doRequest('delete', url, {}, data, cb); self._doRequest('delete', url, {}, wcd, cb);
}); });
}; };

2
lib/client/verifier.js

@ -59,7 +59,9 @@ Verifier.checkTxProposal = function(data, txp) {
var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) { var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) {
if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true; if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true;
}); });
if (!creatorXPubKey) return false; if (!creatorXPubKey) return false;
var creatorSigningPubKey = (new Bitcore.HDPublicKey(creatorXPubKey)).derive('m/1/1').publicKey.toString(); var creatorSigningPubKey = (new Bitcore.HDPublicKey(creatorXPubKey)).derive('m/1/1').publicKey.toString();
var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.encryptedMessage || txp.message); var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.encryptedMessage || txp.message);

4
lib/clienterror.js

@ -18,4 +18,8 @@ function ClientError() {
} }
}; };
ClientError.prototype.toString = function() {
return '<ClientError:' + this.code + ' ' + this.message + '>';
};
module.exports = ClientError; module.exports = ClientError;

4
lib/expressapp.js

@ -64,11 +64,11 @@ ExpressApp.start = function(opts) {
var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400; var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400;
if (!opts.disableLogs) if (!opts.disableLogs)
log.error('Err: ' + status + ':' + req.url + ' :' + err.code + ':' + err.message); log.info('Client Err: ' + status + ' ' + req.url + ' ' + err);
res.status(status).json({ res.status(status).json({
code: err.code, code: err.code,
error: err.message, message: err.message,
}).end(); }).end();
} else { } else {
var code, message; var code, message;

5
lib/walletutils.js

@ -138,4 +138,9 @@ WalletUtils.decryptMessage = function(cyphertextJson, encryptingKey) {
return sjcl.decrypt(key, cyphertextJson); return sjcl.decrypt(key, cyphertextJson);
}; };
WalletUtils.privateKeyToAESKey = function(privKey) {
var pk = Bitcore.PrivateKey.fromString(privKey);
return Bitcore.crypto.Hash.sha256(pk.toBuffer()).slice(0, 16).toString('base64');
};
module.exports = WalletUtils; module.exports = WalletUtils;

175
test/integration/clientApi.js

@ -377,6 +377,7 @@ describe('client API ', function() {
should.not.exist(err); should.not.exist(err);
clients[1].import(str, function(err, wallet) { clients[1].import(str, function(err, wallet) {
should.not.exist(err); should.not.exist(err);
clients[1].createAddress(function(err, x0) { clients[1].createAddress(function(err, x0) {
err.code.should.equal('NOTAUTHORIZED'); err.code.should.equal('NOTAUTHORIZED');
clients[0].createAddress(function(err, x0) { clients[0].createAddress(function(err, x0) {
@ -439,7 +440,7 @@ describe('client API ', function() {
}); });
}); });
}); });
describe('Air gapped flows', function() { describe('Air gapped related flows', function() {
it('should be able get Tx proposals from a file', function(done) { it('should be able get Tx proposals from a file', function(done) {
helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { helpers.createAndJoinWallet(clients, 1, 2, function(err, w) {
should.not.exist(err); should.not.exist(err);
@ -458,7 +459,9 @@ describe('client API ', function() {
}, function(err, txs, rawTxps) { }, function(err, txs, rawTxps) {
should.not.exist(err); should.not.exist(err);
clients[0].parseTxProposals(rawTxps, function(err, txs2) { clients[0].parseTxProposals({
txps: rawTxps
}, function(err, txs2) {
should.not.exist(err); should.not.exist(err);
txs[0].should.deep.equal(txs2[0]); txs[0].should.deep.equal(txs2[0]);
done(); done();
@ -490,7 +493,9 @@ describe('client API ', function() {
//Tamper //Tamper
rawTxps[0].amount++; rawTxps[0].amount++;
clients[0].parseTxProposals(rawTxps, function(err, txs2) { clients[0].parseTxProposals({
txps: rawTxps
}, function(err, txs2) {
err.code.should.equal('SERVERCOMPROMISED'); err.code.should.equal('SERVERCOMPROMISED');
done(); done();
}); });
@ -500,43 +505,109 @@ describe('client API ', function() {
}); });
}); });
}); });
it('should be able export signatures and sign later from a ro client',
function(done) { it('should create from proxy from airgapped', function(done) {
helpers.createAndJoinWallet(clients, 1, 1, function(err, w) {
var airgapped = clients[0];
var proxy = clients[1];
airgapped.generateKey('testnet', function(err) {
should.not.exist(err); should.not.exist(err);
clients[0].createAddress(function(err, x0) { airgapped.export({
should.not.exist(err); access: 'readwrite'
blockExplorerMock.setUtxo(x0, 1, 1); }, function(err, str) {
blockExplorerMock.setUtxo(x0, 1, 2); proxy.import(str, function(err) {
var opts = {
amount: 150000000,
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hello 1-1',
};
clients[0].sendTxProposal(opts, function(err, txp) {
should.not.exist(err); 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; 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();
});
});
});
});
});
it('should join from proxy from airgapped', function(done) {
// Make client RO var airgapped = clients[0];
var data = JSON.parse(fsmock._get('client0')); var proxy = clients[1];
delete data.xPrivKey; var other = clients[2]; // Other copayer
fsmock._set('client0', JSON.stringify(data));
clients[0].signTxProposal(txp, function(err, txp) { 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.not.exist(err);
txp.status.should.equal('broadcasted'); // 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(); done();
}); })
}); });
}); });
}); });
}); });
}); });
it('should be able export signatures and sign later from a ro client',
function(done) {
helpers.createAndJoinWallet(clients, 1, 1, function(err, w) {
should.not.exist(err);
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
blockExplorerMock.setUtxo(x0, 1, 1);
blockExplorerMock.setUtxo(x0, 1, 2);
var opts = {
amount: 150000000,
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hello 1-1',
};
clients[0].sendTxProposal(opts, function(err, txp) {
should.not.exist(err);
clients[0].getSignatures(txp, function(err, signatures) {
should.not.exist(err);
signatures.length.should.equal(txp.inputs.length);
signatures[0].length.should.above(62 * 2);
txp.signatures = signatures;
// Make client RO
var data = JSON.parse(fsmock._get('client0'));
delete data.xPrivKey;
fsmock._set('client0', JSON.stringify(data));
clients[0].signTxProposal(txp, function(err, txp) {
should.not.exist(err);
txp.status.should.equal('broadcasted');
done();
});
});
});
});
});
});
}); });
describe('Address Creation', function() { describe('Address Creation', function() {
@ -639,9 +710,9 @@ describe('client API ', function() {
it('round trip #import #export', function(done) { it('round trip #import #export', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
should.not.exist(err); should.not.exist(err);
clients[0].export({}, function(err, str) { clients[1].export({}, function(err, str) {
should.not.exist(err); should.not.exist(err);
var original = JSON.parse(fsmock._get('client0')); var original = JSON.parse(fsmock._get('client1'));
clients[2].import(str, function(err, wallet) { clients[2].import(str, function(err, wallet) {
should.not.exist(err); should.not.exist(err);
var clone = JSON.parse(fsmock._get('client2')); var clone = JSON.parse(fsmock._get('client2'));
@ -654,7 +725,7 @@ describe('client API ', function() {
}); });
}); });
it('should recreate a wallet, create addresses and receive money', function(done) { it('should recreate a wallet, create addresses and receive money', function(done) {
var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]';
clients[0].import(backup, function(err, wallet) { clients[0].import(backup, function(err, wallet) {
should.not.exist(err); should.not.exist(err);
clients[0].reCreateWallet('pepe', function(err, wallet) { clients[0].reCreateWallet('pepe', function(err, wallet) {
@ -977,29 +1048,29 @@ describe('client API ', function() {
}; };
clients[0].sendTxProposal(opts, function(err, x) { clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err); should.not.exist(err);
clients[0].getStatus( function(err, st) { clients[0].getStatus(function(err, st) {
should.not.exist(err); should.not.exist(err);
var x = st.pendingTxps[0]; var x = st.pendingTxps[0];
x.status.should.equal('pending'); x.status.should.equal('pending');
x.requiredRejections.should.equal(2); x.requiredRejections.should.equal(2);
x.requiredSignatures.should.equal(2); x.requiredSignatures.should.equal(2);
var w = st.wallet; var w = st.wallet;
w.copayers.length.should.equal(3); w.copayers.length.should.equal(3);
w.status.should.equal('complete'); w.status.should.equal('complete');
var b = st.balance; var b = st.balance;
b.totalAmount.should.equal(1000000000); b.totalAmount.should.equal(1000000000);
b.lockedAmount.should.equal(1000000000); b.lockedAmount.should.equal(1000000000);
clients[0].signTxProposal(x, function(err, tx) { clients[0].signTxProposal(x, function(err, tx) {
should.not.exist(err, err); should.not.exist(err, err);
tx.status.should.equal('pending'); tx.status.should.equal('pending');
clients[1].signTxProposal(x, function(err, tx) { clients[1].signTxProposal(x, function(err, tx) {
should.not.exist(err); should.not.exist(err);
tx.status.should.equal('broadcasted'); tx.status.should.equal('broadcasted');
tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id); tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id);
done(); done();
}); });
}); });
}); });
}); });

Loading…
Cancel
Save