diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index 565168831..68e5bbf42 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -18,10 +18,7 @@ package com.google.bitcoin.core; import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; -import com.google.bitcoin.crypto.DeterministicKey; -import com.google.bitcoin.crypto.KeyCrypter; -import com.google.bitcoin.crypto.KeyCrypterException; -import com.google.bitcoin.crypto.KeyCrypterScrypt; +import com.google.bitcoin.crypto.*; import com.google.bitcoin.params.UnitTestParams; import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; @@ -886,6 +883,20 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } } + /** + * Returns a key for the given HD path, assuming it's already been derived. You normally shouldn't use this: + * use currentReceiveKey/freshReceiveKey instead. + */ + public DeterministicKey getKeyByPath(List path) { + lock.lock(); + try { + maybeUpgradeToHD(); + return keychain.getActiveKeyChain().getKeyByPath(path, false); + } finally { + lock.unlock(); + } + } + /** * Convenience wrapper around {@link Wallet#encrypt(com.google.bitcoin.crypto.KeyCrypter, * org.spongycastle.crypto.params.KeyParameter)} which uses the default Scrypt key derivation algorithm and diff --git a/core/src/main/java/com/google/bitcoin/crypto/HDKeyDerivation.java b/core/src/main/java/com/google/bitcoin/crypto/HDKeyDerivation.java index 34547fc4e..1099a29cf 100644 --- a/core/src/main/java/com/google/bitcoin/crypto/HDKeyDerivation.java +++ b/core/src/main/java/com/google/bitcoin/crypto/HDKeyDerivation.java @@ -24,6 +24,7 @@ import org.spongycastle.math.ec.ECPoint; import java.math.BigInteger; import java.nio.ByteBuffer; +import java.security.SecureRandom; import java.util.Arrays; import static com.google.common.base.Preconditions.checkArgument; @@ -34,6 +35,8 @@ import static com.google.common.base.Preconditions.checkState; * deterministic wallet child key generation algorithm. */ public final class HDKeyDerivation { + // Some arbitrary random number. Doesn't matter what it is. + private static final BigInteger RAND_INT = new BigInteger(256, new SecureRandom()); private HDKeyDerivation() { } @@ -43,7 +46,7 @@ public final class HDKeyDerivation { */ public static final int MAX_CHILD_DERIVATION_ATTEMPTS = 100; - private static final HMac MASTER_HMAC_SHA512 = HDUtils.createHmacSha512Digest("Bitcoin seed".getBytes()); + public static final HMac MASTER_HMAC_SHA512 = HDUtils.createHmacSha512Digest("Bitcoin seed".getBytes()); /** * Generates a new deterministic key from the given seed, which can be any arbitrary byte array. However resist @@ -119,7 +122,7 @@ public final class HDKeyDerivation { */ public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException { if (parent.isPubKeyOnly()) { - RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber); + RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber, PublicDeriveMode.NORMAL); return new DeterministicKey( HDUtils.append(parent.getPath(), childNumber), rawKey.chainCode, @@ -136,7 +139,7 @@ public final class HDKeyDerivation { } } - private static RawKeyBytes deriveChildKeyBytesFromPrivate(DeterministicKey parent, + public static RawKeyBytes deriveChildKeyBytesFromPrivate(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException { checkArgument(parent.hasPrivKey(), "Parent key must have private key bytes for this method."); byte[] parentPublicKey = ECKey.compressPoint(parent.getPubKeyPoint()).getEncoded(); @@ -160,7 +163,12 @@ public final class HDKeyDerivation { return new RawKeyBytes(ki.toByteArray(), chainCode); } - private static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException { + public enum PublicDeriveMode { + NORMAL, + WITH_INVERSION + } + + public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber, PublicDeriveMode mode) throws HDDerivationException { checkArgument(!childNumber.isHardened(), "Can't use private derivation with public keys only."); byte[] parentPublicKey = ECKey.compressPoint(parent.getPubKeyPoint()).getEncoded(); assert parentPublicKey.length == 33 : parentPublicKey.length; @@ -173,7 +181,27 @@ public final class HDKeyDerivation { byte[] chainCode = Arrays.copyOfRange(i, 32, 64); BigInteger ilInt = new BigInteger(1, il); assertLessThanN(ilInt, "Illegal derived key: I_L >= n"); - ECPoint Ki = ECKey.CURVE.getG().multiply(ilInt).add(parent.getPubKeyPoint()); + + final ECPoint G = ECKey.CURVE.getG(); + final BigInteger N = ECKey.CURVE.getN(); + ECPoint Ki; + switch (mode) { + case NORMAL: + Ki = G.multiply(ilInt).add(parent.getPubKeyPoint()); + break; + case WITH_INVERSION: + // This trick comes from Gregory Maxwell. Check the homomorphic properties of our curve hold. The + // below calculations should be redundant and give the same result as NORMAL but if the precalculated + // tables have taken a bit flip will yield a different answer. This mode is used when vending a key + // to perform a last-ditch sanity check trying to catch bad RAM. + Ki = G.multiply(ilInt.add(RAND_INT)); + BigInteger additiveInverse = RAND_INT.negate().mod(N); + Ki = Ki.add(G.multiply(additiveInverse)); + Ki = Ki.add(parent.getPubKeyPoint()); + break; + default: throw new AssertionError(); + } + assertNonInfinity(Ki, "Illegal derived key: derived public key equals infinity."); return new RawKeyBytes(Ki.getEncoded(true), chainCode); } @@ -193,10 +221,10 @@ public final class HDKeyDerivation { throw new HDDerivationException(errorMessage); } - private static class RawKeyBytes { - private final byte[] keyBytes, chainCode; + public static class RawKeyBytes { + public final byte[] keyBytes, chainCode; - private RawKeyBytes(byte[] keyBytes, byte[] chainCode) { + public RawKeyBytes(byte[] keyBytes, byte[] chainCode) { this.keyBytes = keyBytes; this.chainCode = chainCode; } diff --git a/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java b/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java index 788516dbb..e309cc916 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java @@ -33,10 +33,7 @@ import org.spongycastle.math.ec.ECPoint; import javax.annotation.Nullable; import java.math.BigInteger; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.Executor; import java.util.concurrent.locks.ReentrantLock; @@ -369,7 +366,14 @@ public class DeterministicKeyChain implements EncryptableKeyChain { List keys = new ArrayList(numberOfKeys); for (int i = 0; i < numberOfKeys; i++) { ImmutableList path = HDUtils.append(parentKey.getPath(), new ChildNumber(index - numberOfKeys + i, false)); - keys.add(hierarchy.get(path, false, false)); + DeterministicKey k = hierarchy.get(path, false, false); + // Just a last minute sanity check before we hand the key out to the app for usage. This isn't inspired + // by any real problem reports from bitcoinj users, but I've heard of cases via the grapevine of + // places that lost money due to bitflips causing addresses to not match keys. Of course in an + // environment with flaky RAM there's no real way to always win: bitflips could be introduced at any + // other layer. But as we're potentially retrieving from long term storage here, check anyway. + checkForBitFlip(k); + keys.add(k); } return keys; } finally { @@ -377,6 +381,14 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } } + private void checkForBitFlip(DeterministicKey k) { + DeterministicKey parent = checkNotNull(k.getParent()); + byte[] rederived = HDKeyDerivation.deriveChildKeyBytesFromPublic(parent, k.getChildNumber(), HDKeyDerivation.PublicDeriveMode.WITH_INVERSION).keyBytes; + byte[] actual = k.getPubKey(); + if (!Arrays.equals(rederived, actual)) + throw new IllegalStateException(String.format("Bit-flip check failed: %s vs %s", Arrays.toString(rederived), Arrays.toString(actual))); + } + private void addToBasicChain(DeterministicKey key) { basicKeyChain.importKeys(ImmutableList.of(key)); } @@ -471,12 +483,12 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } /** Returns the deterministic key for the given absolute path in the hierarchy. */ - protected DeterministicKey getKeyByPath(ImmutableList path) { + protected DeterministicKey getKeyByPath(List path) { return getKeyByPath(path, false); } /** Returns the deterministic key for the given absolute path in the hierarchy, optionally creating it */ - public DeterministicKey getKeyByPath(ImmutableList path, boolean create) { + public DeterministicKey getKeyByPath(List path, boolean create) { return hierarchy.get(path, false, create); } diff --git a/core/src/test/java/com/google/bitcoin/crypto/ChildKeyDerivationTest.java b/core/src/test/java/com/google/bitcoin/crypto/ChildKeyDerivationTest.java index 508f727ee..828859b45 100644 --- a/core/src/test/java/com/google/bitcoin/crypto/ChildKeyDerivationTest.java +++ b/core/src/test/java/com/google/bitcoin/crypto/ChildKeyDerivationTest.java @@ -125,6 +125,15 @@ public class ChildKeyDerivationTest { } } + @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); + assertArrayEquals(key2.keyBytes, key3.keyBytes); + assertArrayEquals(key2.chainCode, key3.chainCode); + } + @Test public void encryptedDerivation() throws Exception { // Check that encrypting a parent key in the heirarchy and then deriving from it yields a DeterministicKey