From 2637b78e89bfb5087116997194ea5c7d76b1c357 Mon Sep 17 00:00:00 2001 From: andres Date: Sun, 14 Feb 2016 16:34:32 -0500 Subject: [PATCH 01/10] added update key (for splited keys) which allows to change passwords and scheme of splited key in json file generated by splitkeys --- src/bgcl.js | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/src/bgcl.js b/src/bgcl.js index d97d30b..3a73270 100755 --- a/src/bgcl.js +++ b/src/bgcl.js @@ -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 updateKey = subparsers.addParser('updatekey', { + addHelp: true, + help: "Update key passwords/schema from an output file of 'splitkeys'" + }); + updateKey.addArgument(['-m'], { help: 'number of shares required to reconstruct a key' }); + updateKey.addArgument(['-n'], { help: 'total number of shares per key' }); + updateKey.addArgument(['-f', '--file'], { help: 'the input file (JSON format)'}); + updateKey.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" @@ -2288,7 +2297,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, @@ -2392,6 +2401,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. @@ -2497,6 +2507,135 @@ BGCL.prototype.handleRecoverKeys = function() { }); }; +/** + * update key passwords from the JSON file produced by splitkeys + */ +BGCL.prototype.handleUpdateKey = 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(); + //var xprv = self.args.verifyonly ? undefined : extendedKey.toBase58(); + if (xpub !== key.xpub) { + throw new Error("xpubs don't match for key " + 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(){ + return getEncryptPassword(0, input.n); + }) + .then(function(){ + var extendedKey = bitcoin.HDNode.fromSeedHex(seed); + input['key'] = { + seed: seed, + xpub: extendedKey.neutered().toBase58(), + xprv: extendedKey.toBase58() + } + var cryptedKey = self.genSplitKey(input); + 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 {*} @@ -2972,6 +3111,8 @@ BGCL.prototype.runCommandHandler = function(cmd) { return this.handleRecoverKeys(); case 'recoverkeys': return this.handleRecoverKeys(); + case 'updatekey': + return this.handleUpdateKey(); case 'dumpwalletuserkey': return this.handleDumpWalletUserKey(); case 'newwallet': From 66fc9967c20bc57c3c5a2726271b7c9063d08b89 Mon Sep 17 00:00:00 2001 From: andres Date: Sun, 14 Feb 2016 18:20:31 -0500 Subject: [PATCH 02/10] fix put index in key to write in json file and minor changes in the outputs --- src/bgcl.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bgcl.js b/src/bgcl.js index 3a73270..6e758bb 100755 --- a/src/bgcl.js +++ b/src/bgcl.js @@ -2608,7 +2608,7 @@ BGCL.prototype.handleUpdateKey = function() { var xpub = extendedKey.neutered().toBase58(); //var xprv = self.args.verifyonly ? undefined : extendedKey.toBase58(); if (xpub !== key.xpub) { - throw new Error("xpubs don't match for key " + key.index); + 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)) @@ -2620,9 +2620,12 @@ BGCL.prototype.handleUpdateKey = function() { 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, @@ -2630,6 +2633,7 @@ BGCL.prototype.handleUpdateKey = function() { 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); From e886fdb7214ba5236005609b9d4cf182d629c169 Mon Sep 17 00:00:00 2001 From: andres Date: Sun, 14 Feb 2016 18:32:51 -0500 Subject: [PATCH 03/10] change in help texts --- src/bgcl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bgcl.js b/src/bgcl.js index 6e758bb..c55d38a 100755 --- a/src/bgcl.js +++ b/src/bgcl.js @@ -565,8 +565,8 @@ BGCL.prototype.createArgumentParser = function() { addHelp: true, help: "Update key passwords/schema from an output file of 'splitkeys'" }); - updateKey.addArgument(['-m'], { help: 'number of shares required to reconstruct a key' }); - updateKey.addArgument(['-n'], { help: 'total number of shares per key' }); + updateKey.addArgument(['-m'], { help: 'new number of shares required to reconstruct a key' }); + updateKey.addArgument(['-n'], { help: 'new total number of shares per key' }); updateKey.addArgument(['-f', '--file'], { help: 'the input file (JSON format)'}); updateKey.addArgument(['-k', '--key'], { help: 'key index to update' }); From 0341f9a703ce36cc830e4721da81a1685ebacb72 Mon Sep 17 00:00:00 2001 From: andres Date: Mon, 15 Feb 2016 09:34:46 -0500 Subject: [PATCH 04/10] update README.md with updateky info --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 0cbeb2e..9223434 100644 --- a/README.md +++ b/README.md @@ -417,6 +417,20 @@ Optional arguments: -k KEYS, --keys KEYS comma-separated list of key indices to recover ``` +## updatekey +A client-side utility to change passwords and schema for key generated by splitkeys. +``` +$ bitgo updatekey -h +usage: bitgo updatekey [-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. From 42dc72200b97f51de76a63ee2029191d5d0ed94e Mon Sep 17 00:00:00 2001 From: andres Date: Mon, 15 Feb 2016 09:38:13 -0500 Subject: [PATCH 05/10] update 2 README.md with updateky info --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9223434..c6cceee 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ 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' + updatekey Update password 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 From 585b734b2e48c903146e2d83151dacd93bb9ff45 Mon Sep 17 00:00:00 2001 From: andres Date: Mon, 15 Feb 2016 09:39:09 -0500 Subject: [PATCH 06/10] fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6cceee..e2dada3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ 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' - updatekey Update password and schema of a key from an output + updatekey 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 From 0a77b732fa1b88fd46e175fbc74148592f66bf57 Mon Sep 17 00:00:00 2001 From: andres Date: Mon, 15 Feb 2016 15:13:09 -0500 Subject: [PATCH 07/10] delete commented line --- src/bgcl.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bgcl.js b/src/bgcl.js index c55d38a..80a82f0 100755 --- a/src/bgcl.js +++ b/src/bgcl.js @@ -2606,7 +2606,6 @@ BGCL.prototype.handleUpdateKey = function() { } var extendedKey = bitcoin.HDNode.fromSeedHex(seed); var xpub = extendedKey.neutered().toBase58(); - //var xprv = self.args.verifyonly ? undefined : extendedKey.toBase58(); if (xpub !== key.xpub) { throw new Error("xpubs don't match for key " + index); } From 252fdb79429f76b9178ed7f598af05393a4a35a0 Mon Sep 17 00:00:00 2001 From: andres Date: Mon, 15 Feb 2016 16:04:14 -0500 Subject: [PATCH 08/10] change name of the function from updatekey to updatesplitkey --- README.md | 8 ++++---- src/bgcl.js | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e2dada3..2586cb9 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ 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' - updatekey Update passwords and schema of a key from an output + 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 @@ -419,11 +419,11 @@ Optional arguments: -k KEYS, --keys KEYS comma-separated list of key indices to recover ``` -## updatekey +## updatesplitkey A client-side utility to change passwords and schema for key generated by splitkeys. ``` -$ bitgo updatekey -h -usage: bitgo updatekey [-h] [-m M] [-n N] [-f FILE] [-k KEY] +$ 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. diff --git a/src/bgcl.js b/src/bgcl.js index 80a82f0..e92ed39 100755 --- a/src/bgcl.js +++ b/src/bgcl.js @@ -561,14 +561,14 @@ 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 updateKey = subparsers.addParser('updatekey', { + var updateSplitKey = subparsers.addParser('updatesplitkey', { addHelp: true, help: "Update key passwords/schema from an output file of 'splitkeys'" }); - updateKey.addArgument(['-m'], { help: 'new number of shares required to reconstruct a key' }); - updateKey.addArgument(['-n'], { help: 'new total number of shares per key' }); - updateKey.addArgument(['-f', '--file'], { help: 'the input file (JSON format)'}); - updateKey.addArgument(['-k', '--key'], { help: 'key index to update' }); + 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, @@ -2510,7 +2510,7 @@ BGCL.prototype.handleRecoverKeys = function() { /** * update key passwords from the JSON file produced by splitkeys */ -BGCL.prototype.handleUpdateKey = function() { +BGCL.prototype.handleUpdateSplitKey = function() { var self = this; var input = new UserInput(this.args); var passwords = []; @@ -3114,8 +3114,8 @@ BGCL.prototype.runCommandHandler = function(cmd) { return this.handleRecoverKeys(); case 'recoverkeys': return this.handleRecoverKeys(); - case 'updatekey': - return this.handleUpdateKey(); + case 'updatesplitkey': + return this.handleUpdateSplitKey(); case 'dumpwalletuserkey': return this.handleDumpWalletUserKey(); case 'newwallet': From deec64b7b7af188d8571256dc1d58fa730f65b11 Mon Sep 17 00:00:00 2001 From: andres Date: Wed, 16 Mar 2016 21:25:01 -0500 Subject: [PATCH 09/10] addred cretxfromjson feature --- src/bgcl.js | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/bgcl.js b/src/bgcl.js index e92ed39..ce0641d 100755 --- a/src/bgcl.js +++ b/src/bgcl.js @@ -588,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' @@ -1877,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); @@ -3128,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': From 3d9a2c9c0832bfe1f43aadd9ee57af792af7287a Mon Sep 17 00:00:00 2001 From: andres Date: Fri, 10 Jun 2016 14:56:39 -0500 Subject: [PATCH 10/10] update README for createtxfromjson option --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2586cb9..71dc998 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ subcommands: 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