From ca033e3368f6ca0b43ab753dcd549495266e3aea Mon Sep 17 00:00:00 2001
From: Andreas Schildbach <andreas.schildbach@bloq.com>
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<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. */
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<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();
     }
 
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.