Skip to content
This repository has been archived by the owner on Jan 23, 2024. It is now read-only.

update passwords and schema for splited keys and create tx to multiple address through a json string #3

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,16 @@ subcommands:
verifysplitkeys Verify the public keys contained in the output file
from the splitkeys command
recoverkeys Recover key(s) from an output file of 'splitkeys'
updatesplitkey Update passwords and schema of a key from an output
file generated by 'splitkeys'
dumpwalletuserkey Dumps a user xprv given a wallet and passphrase
newwallet Create a new Multi-Sig HD wallet
shell Run the BitGo command shell
help Display help
createtx Create an unsigned transaction (online) for signing
(the signing can be done offline)
createtxfromjson Create unsigned transaction (online) to many addresses
using json form {str addr: int value_in_satoshis, ...}
signtx Sign a transaction (can be used offline) with an
input transaction JSON file
sendtx Send a transaction for co-signing to BitGo
Expand Down Expand Up @@ -417,6 +421,20 @@ Optional arguments:
-k KEYS, --keys KEYS comma-separated list of key indices to recover
```

## updatesplitkey
A client-side utility to change passwords and schema for key generated by splitkeys.
```
$ bitgo updatesplitkey -h
usage: bitgo updatesplitkey [-h] [-m M] [-n N] [-f FILE] [-k KEY]

Optional arguments:
-h, --help Show this help message and exit.
-m M new number of shares required to reconstruct a key
-n N new total number of shares per key
-f FILE, --file FILE the input file (JSON format)
-k KEY, --key KEY key index to update
```

## dumpwalletuserkey
Print a wallet's xprv, which is decrypted using the passphrase. If the wallet's encrypted keychain
is not stored by BitGo, there will be no xprv to print.
Expand Down
240 changes: 239 additions & 1 deletion src/bgcl.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,15 @@ BGCL.prototype.createArgumentParser = function() {
recoverKeys.addArgument(['-f', '--file'], { help: 'the input file (JSON format)'});
recoverKeys.addArgument(['-k', '--keys'], { help: 'comma-separated list of key indices to recover' });

var updateSplitKey = subparsers.addParser('updatesplitkey', {
addHelp: true,
help: "Update key passwords/schema from an output file of 'splitkeys'"
});
updateSplitKey.addArgument(['-m'], { help: 'new number of shares required to reconstruct a key' });
updateSplitKey.addArgument(['-n'], { help: 'new total number of shares per key' });
updateSplitKey.addArgument(['-f', '--file'], { help: 'the input file (JSON format)'});
updateSplitKey.addArgument(['-k', '--key'], { help: 'key index to update' });

var dumpWalletUserKey = subparsers.addParser('dumpwalletuserkey', {
addHelp: true,
help: "Dumps the user's private key (first key in the 3 multi-sig keys) to the output"
Expand All @@ -579,6 +588,17 @@ BGCL.prototype.createArgumentParser = function() {
createTx.addArgument(['-p', '--prefix'], { help: 'output file prefix' });
createTx.addArgument(['-u', '--unconfirmed'], { nargs: 0, help: 'allow spending unconfirmed external inputs'});

var createTxFromJson = subparsers.addParser('createtxfromjson', {
addHelp: true,
help: "Create unsigned transaction (online) to many addresses using json form {str addr: int value_in_satoshis, ...}"

});
createTxFromJson.addArgument(['-j', '--json'], {help: 'json string {str addr: int value_in_satoshis, ...}'});
createTxFromJson.addArgument(['-f', '--fee'], {help:'fee to pay for transaction'});
createTxFromJson.addArgument(['-c', '--comment'], {help: 'optional private comment'});
createTxFromJson.addArgument(['-p', '--prefix'], { help: 'output file prefix' });
createTxFromJson.addArgument(['-u', '--unconfirmed'], { nargs: 0, help: 'allow spending unconfirmed external inputs'});

var signTx = subparsers.addParser('signtx', {
addHelp: true,
help: 'Sign a transaction (can be used offline) with an input transaction JSON file'
Expand Down Expand Up @@ -1868,6 +1888,87 @@ BGCL.prototype.handleSendCoins = function() {
});
};


BGCL.prototype.handleCreateTxFromJson = function() {
var self = this;
var input = new UserInput(this.args);
var tx_data;

return this.ensureWallet()
.then(function () {
self.walletHeader();
self.info('Create Unsigned Transaction From Json File:\n');
})
.then(input.getVariable('json', 'json string {str address: int value in satoshis,...}:'))
.then(input.getVariable('fee', 'Blockchain fee (blank to use default fee calculation): '))
.then(input.getVariable('comment', 'Optional private comment: '))
.then(function() {
tx_data = JSON.parse(input.json);
for(var address in tx_data[0]) {
if (tx_data.hasOwnProperty(address)) {
try {
bitcoin.Address.fromBase58Check(address);
} catch (e) {
throw new Error('Invalid destination address: ' + address);
}
satoshis = Number(tx_data[address]);
if (isNaN(satoshis)) {
throw new Error('Invalid amount (non-numeric)');
}
}
}
return self.bitgo.wallets().get({ id: self.session.wallet.id() });
})
.then(function(wallet) {
var params = {
recipients: tx_data,
minConfirms: input.unconfirmed ? 0 : 1,
enforceMinConfirmsForChange: false
};

if (input.fee) {
params.fee = Math.floor(Number(input.fee) * 1e8);
if (isNaN(params.fee)) {
throw new Error('Invalid fee (non-numeric)');
}
}

return wallet.createTransaction(params)
.catch(function(err) {
if (err.needsOTP) {
// unlock
return self.handleUnlock()
.then(function() {
// try again
return wallet.createTransaction(params);
});
} else {
throw err;
}
});
})
.then(function(tx) {
self.info('Created unsigned transaction for:\n')
var total = 0;
for (var address in tx_data) {
if (tx_data.hasOwnProperty(address)) {
self.info(address + ' ---> ' + self.toBTC(tx_data[address]) + ' BTC');
total = total + tx_data[address]
}
}
self.info('\nBTC blockchain fee: ' + tx.fee/1e8 + ' BTC\n')
self.info('Total BTC: ' + self.toBTC(total) + '\n')
tx.comment = input.comment;
if (!input.prefix) {
input.prefix = 'tx' + moment().format('YYYYMDHm');
}
var filename = input.prefix + '.json';
fs.writeFileSync(filename, JSON.stringify(tx, null, 2));
console.log('Wrote ' + filename);
});
};


BGCL.prototype.handleCreateTx = function() {
var self = this;
var input = new UserInput(this.args);
Expand Down Expand Up @@ -2288,7 +2389,7 @@ BGCL.prototype.addUserEntropy = function(userString) {
*/
BGCL.prototype.genSplitKey = function(params, index) {
var self = this;
var key = this.genKey();
var key = params.key || this.genKey();
var result = {
xpub: key.xpub,
m: params.m,
Expand Down Expand Up @@ -2392,6 +2493,7 @@ BGCL.prototype.handleRecoverKeys = function() {
var passwords = [];
var keysToRecover;


/**
* Get a password from the user, testing it against encrypted shares
* to determine which (if any) index of the shares it corresponds to.
Expand Down Expand Up @@ -2497,6 +2599,138 @@ BGCL.prototype.handleRecoverKeys = function() {
});
};

/**
* update key passwords from the JSON file produced by splitkeys
*/
BGCL.prototype.handleUpdateSplitKey = function() {
var self = this;
var input = new UserInput(this.args);
var passwords = [];
var key;
var keys;
var index;

var getEncryptPassword = function(i, n) {
if (i === n) {
return;
}
var passwordName = 'password' + i;
return input.getPassword(passwordName, 'Password for share ' + i + ': ', true)()
.then(function() {
return getEncryptPassword(i+1, n);
});
};

/**
* Get a password from the user, testing it against encrypted shares
* to determine which (if any) index of the shares it corresponds to.
*
* @param {Number} i index of the password (0..n-1)
* @param {Number} n total number of passwords needed
* @param {String[]} shares list of encrypted shares
* @returns {Promise}
*/
var getDecryptPassword = function(i, n, shares) {
if (i === n) {
return;
}
var passwordName = 'password' + i;
return input.getPassword(passwordName, 'Password ' + i + ': ', false)()
.then(function() {
var password = input[passwordName];
var found = false;
shares.forEach(function(share, shareIndex) {
try {
sjcl.decrypt(password, share);
if (!passwords.some(function(p) { return p.shareIndex === shareIndex; })) {
passwords.push({shareIndex: shareIndex, password: password});
found = true;
}
} catch (err) {}
});
if (found) {
return getDecryptPassword(i+1, n, shares);
}
console.log('bad password - try again');
delete input[passwordName];
return getDecryptPassword(i, n, shares);
});
};

return Q().then(function() {
console.log('Update Split Key passwords and schema');
console.log();
})
.then(input.getVariable('file', 'Input file (JSON): '))
.then(input.getVariable('key', 'index to update: '))
.then(function() {
// Grab the list of keys from the file
var json = fs.readFileSync(input.file);
keys = JSON.parse(json);

// Determine and validate the index to update
index = parseInt(input.key,10);
if (isNaN(index)) {
throw new Error('invalid index');
}
if (index < 0 || index >= keys.length) {
throw new Error('index out of range: ' + keys.length + ' keys in file');
}

console.log('Processing key: ' + index);

// Get the passwords
key = keys[index];
return getDecryptPassword(0, key.m, key.seedShares);
})
.then(function() {
// Decrypt the shares, recombine into a seed, validating against existing
// xpub.
var shares = passwords.map(function(p, i) {
console.log('Decrypting Key #' + index + ', Part #' + i);
delete input['password' + i];
return sjcl.decrypt(p.password, key.seedShares[p.shareIndex]);
});
if (shares.length === 1) {
seed = shares[0];
} else {
seed = secrets.combine(shares);
}
var extendedKey = bitcoin.HDNode.fromSeedHex(seed);
var xpub = extendedKey.neutered().toBase58();
if (xpub !== key.xpub) {
throw new Error("xpubs don't match for key " + index);
}
})
.then(input.getIntVariable('n', 'Number of shares per key (N) (new eschema): ', true, 1, 10))
.then(function() {
var mMin = 2;
if (input.n === 1) {
mMin = 1;
}
return input.getIntVariable('m', 'Number of shares required to restore key (M <= N) (new eschema): ', true, mMin, input.n)();
})
.then(function(){
console.log("Generating " + input.n + " new shared secrets for key #" + index + ". set the passwords:");
return getEncryptPassword(0, input.n);
})
.then(function(){
// re generate key with new schema and passwords and.
// update keys and wirte them back to original json file.
var extendedKey = bitcoin.HDNode.fromSeedHex(seed);
input['key'] = {
seed: seed,
xpub: extendedKey.neutered().toBase58(),
xprv: extendedKey.toBase58()
}
var cryptedKey = self.genSplitKey(input);
cryptedKey['index'] = index;
keys[index] = cryptedKey;
fs.writeFileSync(input.file, JSON.stringify(keys, null, 2));
console.log('Wrote ' + input.file);
});
};

/**
* Dumps a user xprv given a wallet and passphrase
* @returns {*}
Expand Down Expand Up @@ -2972,6 +3206,8 @@ BGCL.prototype.runCommandHandler = function(cmd) {
return this.handleRecoverKeys();
case 'recoverkeys':
return this.handleRecoverKeys();
case 'updatesplitkey':
return this.handleUpdateSplitKey();
case 'dumpwalletuserkey':
return this.handleDumpWalletUserKey();
case 'newwallet':
Expand All @@ -2984,6 +3220,8 @@ BGCL.prototype.runCommandHandler = function(cmd) {
return this.handleHelp();
case 'createtx':
return this.handleCreateTx();
case 'createtxfromjson':
return this.handleCreateTxFromJson();
case 'signtx':
return this.handleSignTx();
case 'sendtx':
Expand Down