From ca033e3368f6ca0b43ab753dcd549495266e3aea Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Fri, 11 Aug 2017 11:14:30 +0200 Subject: [PATCH] Option to decrypt private keys and seed on the fly if printing a wallet dump of an encrypted wallet. --- .../main/java/org/bitcoinj/core/ECKey.java | 18 ++++++++------- .../org/bitcoinj/crypto/DeterministicKey.java | 5 +++-- .../wallet/DeterministicKeyChain.java | 22 +++++++++++-------- .../org/bitcoinj/wallet/KeyChainGroup.java | 6 ++--- .../org/bitcoinj/wallet/MarriedKeyChain.java | 4 +++- .../main/java/org/bitcoinj/wallet/Wallet.java | 17 ++++++++++---- .../java/org/bitcoinj/core/ECKeyTest.java | 2 +- .../java/org/bitcoinj/tools/WalletTool.java | 16 +++++++++++++- .../org/bitcoinj/tools/wallet-tool-help.txt | 5 +++-- 9 files changed, 64 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/ECKey.java b/core/src/main/java/org/bitcoinj/core/ECKey.java index 99c69c7e2..acf43381b 100644 --- a/core/src/main/java/org/bitcoinj/core/ECKey.java +++ b/core/src/main/java/org/bitcoinj/core/ECKey.java @@ -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"); } } diff --git a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java index a4b2c858a..5043a0b89 100644 --- a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java @@ -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"); } } } diff --git a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java index 11201dbac..36cd9c161 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java @@ -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 words = seed.getMnemonicCode(); + if (includePrivateKeys) { + DeterministicSeed decryptedSeed = seed.isEncrypted() + ? seed.decrypt(getKeyCrypter(), DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey) : seed; + final List 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. */ diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java index 4f3d411eb..38470e8f9 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java @@ -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 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(); } diff --git a/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java index b74bd6b3b..42ba6c91c 100644 --- a/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java @@ -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'); diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index 497137337..bc459a808 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -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"); diff --git a/core/src/test/java/org/bitcoinj/core/ECKeyTest.java b/core/src/test/java/org/bitcoinj/core/ECKeyTest.java index bbd463ae0..c66169ec8 100644 --- a/core/src/test/java/org/bitcoinj/core/ECKeyTest.java +++ b/core/src/test/java/org/bitcoinj/core/ECKeyTest.java @@ -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 diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index 949d80848..2fed303ce 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -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() { diff --git a/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt b/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt index 430c1edcc..6cb72693f 100644 --- a/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt +++ b/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt @@ -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.