Option to decrypt private keys and seed on the fly if printing a wallet dump of an encrypted wallet.

This commit is contained in:
Andreas Schildbach 2017-08-11 11:14:30 +02:00
parent 49e6af3bd7
commit ca033e3368
9 changed files with 64 additions and 31 deletions

View File

@ -1224,15 +1224,15 @@ public class ECKey implements EncryptableItem {
@Override
public String toString() {
return toString(false, null);
return toString(false, null, null);
}
/**
* Produce a string rendering of the ECKey INCLUDING the private key.
* Unless you absolutely need the private key it is better for security reasons to just use {@link #toString()}.
*/
public String toStringWithPrivate(NetworkParameters params) {
return toString(true, params);
public String toStringWithPrivate(@Nullable KeyParameter aesKey, NetworkParameters params) {
return toString(true, aesKey, params);
}
public String getPrivateKeyAsHex() {
@ -1247,13 +1247,14 @@ public class ECKey implements EncryptableItem {
return getPrivateKeyEncoded(params).toString();
}
private String toString(boolean includePrivate, NetworkParameters params) {
private String toString(boolean includePrivate, @Nullable KeyParameter aesKey, NetworkParameters params) {
final MoreObjects.ToStringHelper helper = MoreObjects.toStringHelper(this).omitNullValues();
helper.add("pub HEX", getPublicKeyAsHex());
if (includePrivate) {
ECKey decryptedKey = isEncrypted() ? decrypt(checkNotNull(aesKey)) : this;
try {
helper.add("priv HEX", getPrivateKeyAsHex());
helper.add("priv WIF", getPrivateKeyAsWiF(params));
helper.add("priv HEX", decryptedKey.getPrivateKeyAsHex());
helper.add("priv WIF", decryptedKey.getPrivateKeyAsWiF(params));
} catch (IllegalStateException e) {
// TODO: Make hasPrivKey() work for deterministic keys and fix this.
} catch (Exception e) {
@ -1271,7 +1272,8 @@ public class ECKey implements EncryptableItem {
return helper.toString();
}
public void formatKeyWithAddress(boolean includePrivateKeys, StringBuilder builder, NetworkParameters params) {
public void formatKeyWithAddress(boolean includePrivateKeys, @Nullable KeyParameter aesKey, StringBuilder builder,
NetworkParameters params) {
final Address address = toAddress(params);
builder.append(" addr:");
builder.append(address.toString());
@ -1282,7 +1284,7 @@ public class ECKey implements EncryptableItem {
builder.append("\n");
if (includePrivateKeys) {
builder.append(" ");
builder.append(toStringWithPrivate(params));
builder.append(toStringWithPrivate(aesKey, params));
builder.append("\n");
}
}

View File

@ -615,13 +615,14 @@ public class DeterministicKey extends ECKey {
}
@Override
public void formatKeyWithAddress(boolean includePrivateKeys, StringBuilder builder, NetworkParameters params) {
public void formatKeyWithAddress(boolean includePrivateKeys, @Nullable KeyParameter aesKey, StringBuilder builder,
NetworkParameters params) {
final Address address = toAddress(params);
builder.append(" addr:").append(address);
builder.append(" hash160:").append(Utils.HEX.encode(getPubKeyHash()));
builder.append(" (").append(getPathAsString()).append(")\n");
if (includePrivateKeys) {
builder.append(" ").append(toStringWithPrivate(params)).append("\n");
builder.append(" ").append(toStringWithPrivate(aesKey, params)).append("\n");
}
}
}

View File

@ -1311,16 +1311,19 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
throw new UnsupportedOperationException();
}
public String toString(boolean includePrivateKeys, NetworkParameters params) {
public String toString(boolean includePrivateKeys, @Nullable KeyParameter aesKey, NetworkParameters params) {
final DeterministicKey watchingKey = getWatchingKey();
final StringBuilder builder = new StringBuilder();
if (seed != null) {
if (seed.isEncrypted()) {
builder.append("Seed is encrypted\n");
} else if (includePrivateKeys) {
final List<String> words = seed.getMnemonicCode();
if (includePrivateKeys) {
DeterministicSeed decryptedSeed = seed.isEncrypted()
? seed.decrypt(getKeyCrypter(), DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey) : seed;
final List<String> words = decryptedSeed.getMnemonicCode();
builder.append("Seed as words: ").append(Utils.SPACE_JOINER.join(words)).append('\n');
builder.append("Seed as hex: ").append(seed.toHexString()).append('\n');
builder.append("Seed as hex: ").append(decryptedSeed.toHexString()).append('\n');
} else {
if (seed.isEncrypted())
builder.append("Seed is encrypted\n");
}
builder.append("Seed birthday: ").append(seed.getCreationTimeSeconds()).append(" [")
.append(Utils.dateTimeFormat(seed.getCreationTimeSeconds() * 1000)).append("]\n");
@ -1329,13 +1332,14 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
.append(Utils.dateTimeFormat(watchingKey.getCreationTimeSeconds() * 1000)).append("]\n");
}
builder.append("Key to watch: ").append(watchingKey.serializePubB58(params)).append('\n');
formatAddresses(includePrivateKeys, params, builder);
formatAddresses(includePrivateKeys, aesKey, params, builder);
return builder.toString();
}
protected void formatAddresses(boolean includePrivateKeys, NetworkParameters params, StringBuilder builder) {
protected void formatAddresses(boolean includePrivateKeys, @Nullable KeyParameter aesKey, NetworkParameters params,
StringBuilder builder) {
for (ECKey key : getKeys(false, true))
key.formatKeyWithAddress(includePrivateKeys, builder, params);
key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params);
}
/** The number of signatures required to spend coins received by this keychain. */

View File

@ -785,16 +785,16 @@ public class KeyChainGroup implements KeyBag {
}
}
public String toString(boolean includePrivateKeys) {
public String toString(boolean includePrivateKeys, @Nullable KeyParameter aesKey) {
final StringBuilder builder = new StringBuilder();
if (basic != null) {
List<ECKey> keys = basic.getKeys();
Collections.sort(keys, ECKey.AGE_COMPARATOR);
for (ECKey key : keys)
key.formatKeyWithAddress(includePrivateKeys, builder, params);
key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params);
}
for (DeterministicKeyChain chain : chains)
builder.append(chain.toString(includePrivateKeys, params)).append('\n');
builder.append(chain.toString(includePrivateKeys, aesKey, params)).append('\n');
return builder.toString();
}

View File

@ -28,6 +28,7 @@ import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.spongycastle.crypto.params.KeyParameter;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
@ -234,7 +235,8 @@ public class MarriedKeyChain extends DeterministicKeyChain {
}
@Override
protected void formatAddresses(boolean includePrivateKeys, NetworkParameters params, StringBuilder builder2) {
protected void formatAddresses(boolean includePrivateKeys, @Nullable KeyParameter aesKey, NetworkParameters params,
StringBuilder builder2) {
for (DeterministicKeyChain followingChain : followingKeyChains)
builder2.append("Following chain: ").append(followingChain.getWatchingKey().serializePubB58(params))
.append('\n');

View File

@ -3183,20 +3183,29 @@ public class Wallet extends BaseTaggableObject
@Override
public String toString() {
return toString(false, true, true, null);
return toString(false, null, true, true, null);
}
/**
* @deprecated Use {@link #toString(boolean, KeyParameter, boolean, boolean, AbstractBlockChain)} instead.
*/
@Deprecated
public String toString(boolean includePrivateKeys, boolean includeTransactions, boolean includeExtensions,
@Nullable AbstractBlockChain chain) {
return toString(includePrivateKeys, includeTransactions, includeExtensions, chain);
}
/**
* Formats the wallet as a human readable piece of text. Intended for debugging, the format is not meant to be
* stable or human readable.
* @param includePrivateKeys Whether raw private key data should be included.
* @param key for decrypting private key data for if the wallet is encrypted.
* @param includeTransactions Whether to print transaction data.
* @param includeExtensions Whether to print extension data.
* @param chain If set, will be used to estimate lock times for block timelocked transactions.
*/
public String toString(boolean includePrivateKeys, boolean includeTransactions, boolean includeExtensions,
@Nullable AbstractBlockChain chain) {
public String toString(boolean includePrivateKeys, @Nullable KeyParameter aesKey, boolean includeTransactions,
boolean includeExtensions, @Nullable AbstractBlockChain chain) {
lock.lock();
keyChainGroupLock.lock();
try {
@ -3226,7 +3235,7 @@ public class Wallet extends BaseTaggableObject
final Date keyRotationTime = getKeyRotationTime();
if (keyRotationTime != null)
builder.append("Key rotation time: ").append(Utils.dateTimeFormat(keyRotationTime)).append('\n');
builder.append(keyChainGroup.toString(includePrivateKeys));
builder.append(keyChainGroup.toString(includePrivateKeys, aesKey));
if (!watchedScripts.isEmpty()) {
builder.append("\nWatched scripts:\n");

View File

@ -317,7 +317,7 @@ public class ECKeyTest {
ECKey key = ECKey.fromPrivate(BigInteger.TEN).decompress(); // An example private key.
NetworkParameters params = MainNetParams.get();
assertEquals("ECKey{pub HEX=04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7, isEncrypted=false, isPubKeyOnly=false}", key.toString());
assertEquals("ECKey{pub HEX=04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7, priv HEX=000000000000000000000000000000000000000000000000000000000000000a, priv WIF=5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreBoNWTw6, isEncrypted=false, isPubKeyOnly=false}", key.toStringWithPrivate(params));
assertEquals("ECKey{pub HEX=04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7, priv HEX=000000000000000000000000000000000000000000000000000000000000000a, priv WIF=5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreBoNWTw6, isEncrypted=false, isPubKeyOnly=false}", key.toStringWithPrivate(null, params));
}
@Test

View File

@ -1447,7 +1447,21 @@ public class WalletTool {
// there just for the dump case.
if (chainFileName.exists())
setup();
System.out.println(wallet.toString(options.has("dump-privkeys"), true, true, chain));
final boolean dumpPrivkeys = options.has("dump-privkeys");
if (dumpPrivkeys && wallet.isEncrypted()) {
if (password != null) {
final KeyParameter aesKey = passwordToKey(true);
if (aesKey == null)
return; // Error message already printed.
System.out.println(wallet.toString(true, aesKey, true, true, chain));
} else {
System.err.println("Can't dump privkeys, wallet is encrypted.");
return;
}
} else {
System.out.println(wallet.toString(dumpPrivkeys, null, true, true, chain));
}
}
private static void setCreationTime() {

View File

@ -4,8 +4,9 @@ Usage: wallet-tool --flags action-name
wallet-tool action-name --flags
>>> ACTIONS
dump Loads and prints the given wallet in textual form to stdout. Private keys are only printed
if --dump-privkeys is specified.
dump Loads and prints the given wallet in textual form to stdout. Private keys and seed are only
printed if --dump-privkeys is specified. If the wallet is encrypted, also specify the --password
option to dump the private keys and seed.
raw-dump Prints the wallet as a raw protobuf with no parsing or sanity checking applied.
create Makes a new wallet in the file specified by --wallet.
Will complain and require --force if the wallet already exists.