Merge pull request #74 from matiu/airgapped03

Air gapped
This commit is contained in:
Ivan Socolsky 2015-02-23 17:13:06 -03:00
commit e67347d8cc
10 changed files with 416 additions and 231 deletions

View File

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

View File

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

20
bit-wallet/bit-genkey Executable file
View File

@ -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.');
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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