diff --git a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java index e4855d55..3aa6ff27 100644 --- a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java @@ -43,7 +43,7 @@ public class DeterministicKey extends ECKey { private final DeterministicKey parent; private final ImmutableList childNumberPath; private final int depth; - private final int parentFingerprint; // 0 if this key is root node of key hierarchy + private int parentFingerprint; // 0 if this key is root node of key hierarchy /** 32 bytes */ private final byte[] chainCode; @@ -240,13 +240,32 @@ public class DeterministicKey extends ECKey { } /** - * Returns the same key with the private part removed. May return the same instance. + * Returns the same key with the private bytes removed. May return the same instance. The purpose of this is to save + * memory: the private key can always be very efficiently rederived from a parent that a private key, so storing + * all the private keys in RAM is a poor tradeoff especially on constrained devices. This means that the returned + * key may still be usable for signing and so on, so don't expect it to be a true pubkey-only object! If you want + * that then you should follow this call with a call to {@link #dropParent()}. */ - public DeterministicKey getPubOnly() { - if (isPubKeyOnly()) return this; - return new DeterministicKey(getPath(), getChainCode(), pub, null, parent); + public DeterministicKey dropPrivateBytes() { + if (isPubKeyOnly()) + return this; + else + return new DeterministicKey(getPath(), getChainCode(), pub, null, parent); } + /** + *

Returns the same key with the parent pointer removed (it still knows its own path and the parent fingerprint).

+ * + *

If this key doesn't have private key bytes stored/cached itself, but could rederive them from the parent, then + * the new key returned by this method won't be able to do that. Thus, using dropPrivateBytes().dropParent() on a + * regular DeterministicKey will yield a new DeterministicKey that cannot sign or do other things involving the + * private key at all.

+ */ + public DeterministicKey dropParent() { + DeterministicKey key = new DeterministicKey(getPath(), getChainCode(), pub, priv, null); + key.parentFingerprint = parentFingerprint; + return key; + } static byte[] addChecksum(byte[] input) { int inputLength = input.length; diff --git a/core/src/main/java/org/bitcoinj/testing/KeyChainTransactionSigner.java b/core/src/main/java/org/bitcoinj/testing/KeyChainTransactionSigner.java index 57966474..e789fc9b 100644 --- a/core/src/main/java/org/bitcoinj/testing/KeyChainTransactionSigner.java +++ b/core/src/main/java/org/bitcoinj/testing/KeyChainTransactionSigner.java @@ -45,6 +45,6 @@ public class KeyChainTransactionSigner extends CustomTransactionSigner { protected SignatureAndKey getSignature(Sha256Hash sighash, List derivationPath) { ImmutableList keyPath = ImmutableList.copyOf(derivationPath); DeterministicKey key = keyChain.getKeyByPath(keyPath, true); - return new SignatureAndKey(key.sign(sighash), key.getPubOnly()); + return new SignatureAndKey(key.sign(sighash), key.dropPrivateBytes().dropParent()); } } diff --git a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java index befdc5b2..df3ae665 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java @@ -31,7 +31,6 @@ import com.google.protobuf.ByteString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.math.ec.ECPoint; import javax.annotation.Nullable; import java.math.BigInteger; @@ -309,7 +308,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { * this method to watch an arbitrary fragment of some other tree, this limitation may be removed in future. */ public DeterministicKeyChain(DeterministicKey watchingKey, long creationTimeSeconds) { - checkArgument(watchingKey.isPubKeyOnly(), "Private subtrees not currently supported"); + checkArgument(watchingKey.isPubKeyOnly(), "Private subtrees not currently supported: if you got this key from DKC.getWatchingKey() then use .dropPrivate().dropParent() on it first."); checkArgument(watchingKey.getPath().size() == 1, "You can only watch an account key currently"); basicKeyChain = new BasicKeyChain(); this.creationTimeSeconds = creationTimeSeconds; @@ -402,7 +401,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { if (key.getPath().size() != 3) continue; // Not a leaf key. DeterministicKey parent = hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false); // Clone the key to the new encrypted hierarchy. - key = new DeterministicKey(key.getPubOnly(), parent); + key = new DeterministicKey(key.dropPrivateBytes(), parent); hierarchy.putKey(key); basicKeyChain.importKey(key); } @@ -616,13 +615,17 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } /** - *

An alias for getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH).getPubOnly(). - * Use this when you would like to create a watching key chain that follows this one, but can't spend money from it. + *

An alias for getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH).

+ * + *

Use this when you would like to create a watching key chain that follows this one, but can't spend money from it. * The returned key can be serialized and then passed into {@link #watch(org.bitcoinj.crypto.DeterministicKey)} * on another system to watch the hierarchy.

+ * + *

Note that the returned key is not pubkey only unless this key chain already is: the returned key can still + * be used for signing etc if the private key bytes are available.

*/ public DeterministicKey getWatchingKey() { - return getKeyByPath(ACCOUNT_ZERO_PATH).getPubOnly(); + return getKeyByPath(ACCOUNT_ZERO_PATH); } @Override @@ -969,7 +972,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { checkState(key.isEncrypted()); DeterministicKey parent = chain.hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false); // Clone the key to the new decrypted hierarchy. - key = new DeterministicKey(key.getPubOnly(), parent); + key = new DeterministicKey(key.dropPrivateBytes(), parent); chain.hierarchy.putKey(key); chain.basicKeyChain.importKey(key); } @@ -1143,7 +1146,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { int nextChild = numChildren; for (int i = 0; i < needed; i++) { DeterministicKey key = HDKeyDerivation.deriveThisOrNextChildKey(parent, nextChild); - key = key.getPubOnly(); + key = key.dropPrivateBytes(); hierarchy.putKey(key); result.add(key); nextChild = key.getChildNumber().num() + 1; diff --git a/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java b/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java index c503e47b..b14de147 100644 --- a/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java +++ b/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java @@ -17,16 +17,12 @@ package org.bitcoinj.crypto; -import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Sha256Hash; -import org.bitcoinj.params.MainNetParams; -import org.bitcoinj.params.TestNet3Params; -import org.bitcoinj.params.UnitTestParams; -import org.junit.Test; -import org.spongycastle.crypto.params.KeyParameter; +import org.bitcoinj.core.*; +import org.bitcoinj.params.*; +import org.junit.*; +import org.spongycastle.crypto.params.*; -import static org.bitcoinj.core.Utils.HEX; +import static org.bitcoinj.core.Utils.*; import static org.junit.Assert.*; /** @@ -113,27 +109,27 @@ public class ChildKeyDerivationTest { DeterministicKey ekpub_1_IN_4095 = HDKeyDerivation.deriveChildKey(ekpub_1_IN, 4095); // ExtendedHierarchicKey ekpub_1_IN_4bil = HDKeyDerivation.deriveChildKey(ekpub_1_IN, 4294967295L); - assertEquals(hexEncodePub(ekprv.getPubOnly()), hexEncodePub(ekpub)); - assertEquals(hexEncodePub(ekprv_0.getPubOnly()), hexEncodePub(ekpub_0)); - assertEquals(hexEncodePub(ekprv_1.getPubOnly()), hexEncodePub(ekpub_1)); - assertEquals(hexEncodePub(ekprv_0_IN.getPubOnly()), hexEncodePub(ekpub_0_IN)); - assertEquals(hexEncodePub(ekprv_0_IN_0.getPubOnly()), hexEncodePub(ekpub_0_IN_0)); - assertEquals(hexEncodePub(ekprv_0_IN_1.getPubOnly()), hexEncodePub(ekpub_0_IN_1)); - assertEquals(hexEncodePub(ekprv_0_IN_2.getPubOnly()), hexEncodePub(ekpub_0_IN_2)); - assertEquals(hexEncodePub(ekprv_0_EX_0.getPubOnly()), hexEncodePub(ekpub_0_EX_0)); - assertEquals(hexEncodePub(ekprv_0_EX_1.getPubOnly()), hexEncodePub(ekpub_0_EX_1)); - assertEquals(hexEncodePub(ekprv_0_EX_2.getPubOnly()), hexEncodePub(ekpub_0_EX_2)); - assertEquals(hexEncodePub(ekprv_1_IN.getPubOnly()), hexEncodePub(ekpub_1_IN)); - assertEquals(hexEncodePub(ekprv_1_IN_4095.getPubOnly()), hexEncodePub(ekpub_1_IN_4095)); - //assertEquals(hexEncodePub(ekprv_1_IN_4bil.getPubOnly()), hexEncodePub(ekpub_1_IN_4bil)); + assertEquals(hexEncodePub(ekprv.dropPrivateBytes().dropParent()), hexEncodePub(ekpub)); + assertEquals(hexEncodePub(ekprv_0.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0)); + assertEquals(hexEncodePub(ekprv_1.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_1)); + assertEquals(hexEncodePub(ekprv_0_IN.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0_IN)); + assertEquals(hexEncodePub(ekprv_0_IN_0.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0_IN_0)); + assertEquals(hexEncodePub(ekprv_0_IN_1.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0_IN_1)); + assertEquals(hexEncodePub(ekprv_0_IN_2.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0_IN_2)); + assertEquals(hexEncodePub(ekprv_0_EX_0.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0_EX_0)); + assertEquals(hexEncodePub(ekprv_0_EX_1.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0_EX_1)); + assertEquals(hexEncodePub(ekprv_0_EX_2.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_0_EX_2)); + assertEquals(hexEncodePub(ekprv_1_IN.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_1_IN)); + assertEquals(hexEncodePub(ekprv_1_IN_4095.dropPrivateBytes().dropParent()), hexEncodePub(ekpub_1_IN_4095)); + //assertEquals(hexEncodePub(ekprv_1_IN_4bil.dropPrivateBytes()), hexEncodePub(ekpub_1_IN_4bil)); } } @Test public void inverseEqualsNormal() throws Exception { DeterministicKey key1 = HDKeyDerivation.createMasterPrivateKey("Wired / Aug 13th 2014 / Snowden: I Left the NSA Clues, But They Couldn't Find Them".getBytes()); - HDKeyDerivation.RawKeyBytes key2 = HDKeyDerivation.deriveChildKeyBytesFromPublic(key1.getPubOnly(), ChildNumber.ZERO, HDKeyDerivation.PublicDeriveMode.NORMAL); - HDKeyDerivation.RawKeyBytes key3 = HDKeyDerivation.deriveChildKeyBytesFromPublic(key1.getPubOnly(), ChildNumber.ZERO, HDKeyDerivation.PublicDeriveMode.WITH_INVERSION); + HDKeyDerivation.RawKeyBytes key2 = HDKeyDerivation.deriveChildKeyBytesFromPublic(key1.dropPrivateBytes().dropParent(), ChildNumber.ZERO, HDKeyDerivation.PublicDeriveMode.NORMAL); + HDKeyDerivation.RawKeyBytes key3 = HDKeyDerivation.deriveChildKeyBytesFromPublic(key1.dropPrivateBytes().dropParent(), ChildNumber.ZERO, HDKeyDerivation.PublicDeriveMode.WITH_INVERSION); assertArrayEquals(key2.keyBytes, key3.keyBytes); assertArrayEquals(key2.chainCode, key3.chainCode); } @@ -176,8 +172,14 @@ public class ChildKeyDerivationTest { assertFalse(key2.isPubKeyOnly()); DeterministicKey key3 = HDKeyDerivation.deriveChildKey(key2, ChildNumber.ZERO); assertFalse(key3.isPubKeyOnly()); - DeterministicKey pubkey2 = key2.getPubOnly(); - assertTrue(pubkey2.isPubKeyOnly()); + + key2 = key2.dropPrivateBytes(); + assertFalse(key2.isPubKeyOnly()); // still got private key bytes from the parents! + + // pubkey2 got its cached private key bytes (if any) dropped, and now it'll lose its parent too, so now it + // becomes a true pubkey-only object. + DeterministicKey pubkey2 = key2.dropParent(); + DeterministicKey pubkey3 = HDKeyDerivation.deriveChildKey(pubkey2, ChildNumber.ZERO); assertTrue(pubkey3.isPubKeyOnly()); assertEquals(key3.getPubKeyPoint(), pubkey3.getPubKeyPoint()); diff --git a/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java b/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java index a2e58166..ac720107 100644 --- a/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java @@ -273,7 +273,7 @@ public class DeterministicKeyChainTest { @Test(expected = IllegalStateException.class) public void watchingCannotEncrypt() throws Exception { final DeterministicKey accountKey = chain.getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH); - chain = DeterministicKeyChain.watch(accountKey.getPubOnly()); + chain = DeterministicKeyChain.watch(accountKey.dropPrivateBytes().dropParent()); chain = chain.toEncrypted("this doesn't make any sense"); } @@ -303,7 +303,7 @@ public class DeterministicKeyChainTest { DeterministicKey[] keys = new DeterministicKey[100]; for (int i = 0; i < keys.length; i++) keys[i] = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); - chain = DeterministicKeyChain.watch(chain.getWatchingKey()); + chain = DeterministicKeyChain.watch(chain.getWatchingKey().dropPrivateBytes().dropParent()); int e = chain.numBloomFilterEntries(); BloomFilter filter = chain.getFilter(e, 0.001, 1); for (DeterministicKey key : keys)