From 1ff81a3243d28337f616b8cf8f985d12b3b0ed55 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 26 Jan 2012 16:23:29 +0100 Subject: [PATCH] Slight redesign of the ECKey API to allow you to have public only ("watching") keys. --- .../google/bitcoin/core/DumpedPrivateKey.java | 2 +- src/com/google/bitcoin/core/ECKey.java | 108 ++++++++++-------- src/com/google/bitcoin/core/Utils.java | 19 ++- .../google/bitcoin/examples/PrivateKeys.java | 2 +- .../google/bitcoin/core/AlertMessageTest.java | 4 +- tests/com/google/bitcoin/core/ECKeyTest.java | 5 +- 6 files changed, 84 insertions(+), 56 deletions(-) diff --git a/src/com/google/bitcoin/core/DumpedPrivateKey.java b/src/com/google/bitcoin/core/DumpedPrivateKey.java index 82b14d45..9267a42d 100644 --- a/src/com/google/bitcoin/core/DumpedPrivateKey.java +++ b/src/com/google/bitcoin/core/DumpedPrivateKey.java @@ -49,6 +49,6 @@ public class DumpedPrivateKey extends VersionedChecksummedBytes { * Returns an ECKey created from this encoded private key. */ public ECKey getKey() { - return new ECKey(new BigInteger(1, bytes)); + return new ECKey(new BigInteger(1, bytes), null); } } diff --git a/src/com/google/bitcoin/core/ECKey.java b/src/com/google/bitcoin/core/ECKey.java index b597d7aa..d3290823 100644 --- a/src/com/google/bitcoin/core/ECKey.java +++ b/src/com/google/bitcoin/core/ECKey.java @@ -33,6 +33,9 @@ import java.io.Serializable; import java.math.BigInteger; import java.security.SecureRandom; +// TODO: This class is quite a mess by now. Once users are migrated away from Java serialization for the wallets, +// refactor this to have better internal layout and a more consistent API. + /** * Represents an elliptic curve keypair that we own and can use for signing transactions. Currently, * Bouncy Castle is used. In future this may become an interface with multiple implementations using different crypto @@ -45,7 +48,7 @@ public class ECKey implements Serializable { private static final long serialVersionUID = -728224901792295832L; static { - // All clients must agree on the curve to use by agreement. BitCoin uses secp256k1. + // All clients must agree on the curve to use by agreement. Bitcoin uses secp256k1. X9ECParameters params = SECNamedCurves.getByName("secp256k1"); ecParams = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); secureRandom = new SecureRandom(); @@ -54,12 +57,13 @@ public class ECKey implements Serializable { // The two parts of the key. If "priv" is set, "pub" can always be calculated. If "pub" is set but not "priv", we // can only verify signatures not make them. // TODO: Redesign this class to use consistent internals and more efficient serialization. - private final BigInteger priv; - private final byte[] pub; + private BigInteger priv; + private byte[] pub; // Creation time of the key in seconds since the epoch, or zero if the key was deserialized from a version that did // not have this field. private long creationTimeSeconds; + // Transient because it's calculated on demand. transient private byte[] pubKeyHash; /** Generates an entirely new keypair. */ @@ -76,31 +80,12 @@ public class ECKey implements Serializable { creationTimeSeconds = Utils.now().getTime() / 1000; } - /** - * Returns the creation time of this key or zero if the key was deserialized from a version that did not store - * that data. - */ - public long getCreationTimeSeconds() { - return creationTimeSeconds; - } - - /** - * Sets the creation time of this key. Zero is a convention to mean "unavailable". This method can be useful when - * you have a raw key you are importing from somewhere else. - * @param newCreationTimeSeconds - */ - public void setCreationTimeSeconds(long newCreationTimeSeconds) { - if (newCreationTimeSeconds < 0) - throw new IllegalArgumentException("Cannot set creation time to negative value: " + newCreationTimeSeconds); - creationTimeSeconds = newCreationTimeSeconds; - } - /** * Construct an ECKey from an ASN.1 encoded private key. These are produced by OpenSSL and stored by the BitCoin - * reference implementation in its wallet. + * reference implementation in its wallet. Note that this is slow because it requires an EC point multiply. */ public static ECKey fromASN1(byte[] asn1privkey) { - return new ECKey(extractPrivateKeyFromASN1(asn1privkey)); + return new ECKey(extractPrivateKeyFromASN1(asn1privkey), null); } /** @@ -132,12 +117,20 @@ public class ECKey implements Serializable { } /** - * Creates an ECKey given only the private key. This works because EC public keys are derivable from their - * private keys by doing a multiply with the generator value. + * Creates an ECKey given either the private key only, the public key only, or both. If only the private key + * is supplied, the public key will be calculated from it (this is slow). If both are supplied, it's assumed + * the public key already correctly matches the public key. If only the public key is supplied, this ECKey cannot + * be used for signing. */ - public ECKey(BigInteger privKey) { + public ECKey(BigInteger privKey, BigInteger pubKey) { this.priv = privKey; - this.pub = publicKeyFromPrivate(privKey); + this.pub = null; + if (pubKey == null && privKey != null) { + // Derive public from private. + this.pub = publicKeyFromPrivate(privKey); + } else if (pubKey != null) { + this.pub = Utils.bigIntegerTo32Bytes(pubKey); + } } /** @@ -145,12 +138,20 @@ public class ECKey implements Serializable { * is more convenient if you are importing a key from elsewhere. The public key will be automatically derived * from the private key. Same as calling {@link ECKey#fromPrivKeyBytes(byte[])}. */ - public ECKey(byte[] privKeyBytes) { - this(new BigInteger(1, privKeyBytes)); + public ECKey(byte[] privKeyBytes, byte[] pubKeyBytes) { + priv = privKeyBytes == null ? null : new BigInteger(1, privKeyBytes); + pub = pubKeyBytes; + if (pub == null && priv != null) { + // Derive public from private. + pub = publicKeyFromPrivate(priv); + } } - /** Derive the public key by doing a point multiply of G * priv. */ - private static byte[] publicKeyFromPrivate(BigInteger privKey) { + /** + * Returns public key bytes from the given private key. To convert a byte array into a BigInteger, use + * new BigInteger(1, bytes); + */ + public static byte[] publicKeyFromPrivate(BigInteger privKey) { return ecParams.getG().multiply(privKey).getEncoded(); } @@ -172,7 +173,9 @@ public class ECKey implements Serializable { public String toString() { StringBuffer b = new StringBuffer(); b.append("pub:").append(Utils.bytesToHexString(pub)); - b.append(" priv:").append(Utils.bytesToHexString(priv.toByteArray())); + if (priv != null) { + b.append(" priv:").append(Utils.bytesToHexString(priv.toByteArray())); + } return b.toString(); } @@ -188,17 +191,20 @@ public class ECKey implements Serializable { /** * Calcuates an ECDSA signature in DER format for the given input hash. Note that the input is expected to be * 32 bytes long. + * @throws IllegalStateException if this ECKey has only a public key. */ public byte[] sign(byte[] input) { + if (priv == null) + throw new IllegalStateException("This ECKey does not have the private key necessary for signing."); ECDSASigner signer = new ECDSASigner(); ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(priv, ecParams); signer.init(true, privKey); BigInteger[] sigs = signer.generateSignature(input); // What we get back from the signer are the two components of a signature, r and s. To get a flat byte stream - // of the type used by BitCoin we have to encode them using DER encoding, which is just a way to pack the two + // of the type used by Bitcoin we have to encode them using DER encoding, which is just a way to pack the two // components into a structure. try { - //usually 70-72 bytes. + // Usually 70-72 bytes. ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(72); DERSequenceGenerator seq = new DERSequenceGenerator(bos); seq.addObject(new DERInteger(sigs[0])); @@ -272,20 +278,11 @@ public class ECKey implements Serializable { * Returns a 32 byte array containing the private key. */ public byte[] getPrivKeyBytes() { - // Getting the bytes out of a BigInteger gives us an extra zero byte on the end (for signedness) - // or less than 32 bytes (leading zeros). Coerce to 32 bytes in all cases. - byte[] bytes = new byte[32]; - - byte[] privArray = priv.toByteArray(); - int privStart = (privArray.length == 33) ? 1 : 0; - int privLength = Math.min(privArray.length, 32); - System.arraycopy(privArray, privStart, bytes, 32 - privLength, privLength); - - return bytes; + return Utils.bigIntegerTo32Bytes(priv); } public static ECKey fromPrivKeyBytes(byte[] bytes) { - return new ECKey(new BigInteger(1, bytes)); + return new ECKey(new BigInteger(1, bytes), null); } /** @@ -298,4 +295,23 @@ public class ECKey implements Serializable { public DumpedPrivateKey getPrivateKeyEncoded(NetworkParameters params) { return new DumpedPrivateKey(params, getPrivKeyBytes()); } + + /** + * Returns the creation time of this key or zero if the key was deserialized from a version that did not store + * that data. + */ + public long getCreationTimeSeconds() { + return creationTimeSeconds; + } + + /** + * Sets the creation time of this key. Zero is a convention to mean "unavailable". This method can be useful when + * you have a raw key you are importing from somewhere else. + * @param newCreationTimeSeconds + */ + public void setCreationTimeSeconds(long newCreationTimeSeconds) { + if (newCreationTimeSeconds < 0) + throw new IllegalArgumentException("Cannot set creation time to negative value: " + newCreationTimeSeconds); + creationTimeSeconds = newCreationTimeSeconds; + } } diff --git a/src/com/google/bitcoin/core/Utils.java b/src/com/google/bitcoin/core/Utils.java index 67a696fa..d694a349 100644 --- a/src/com/google/bitcoin/core/Utils.java +++ b/src/com/google/bitcoin/core/Utils.java @@ -16,6 +16,8 @@ package com.google.bitcoin.core; +import org.bouncycastle.crypto.digests.RIPEMD160Digest; + import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; @@ -24,8 +26,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; -import org.bouncycastle.crypto.digests.RIPEMD160Digest; - /** * A collection of various utility methods that are helpful for working with the BitCoin protocol. * To enable debug logging from the library, run with -Dbitcoinj.logging=true on your command line. @@ -61,6 +61,21 @@ public class Utils { return bi; } + /** + * The regular {@link java.math.BigInteger#toByteArray()} method isn't quite what we often need: it appends a + * leading zero to indicate that the number is positive and may need padding. + * @param b + * @return 32 byte long array. + */ + public static byte[] bigIntegerTo32Bytes(BigInteger b) { + byte[] bytes = new byte[32]; + byte[] biBytes = b.toByteArray(); + int start = (biBytes.length == 33) ? 1 : 0; + int length = Math.min(biBytes.length, 32); + System.arraycopy(biBytes, start, bytes, 32 - length, length); + return bytes; + } + /** * Convert an amount expressed in the way humans are used to into nanocoins.

*

diff --git a/src/com/google/bitcoin/examples/PrivateKeys.java b/src/com/google/bitcoin/examples/PrivateKeys.java index 58fe84a3..2c4c0188 100644 --- a/src/com/google/bitcoin/examples/PrivateKeys.java +++ b/src/com/google/bitcoin/examples/PrivateKeys.java @@ -44,7 +44,7 @@ public class PrivateKeys { key = dumpedPrivateKey.getKey(); } else { BigInteger privKey = Base58.decodeToBigInteger(args[0]); - key = new ECKey(privKey); + key = new ECKey(privKey, null); } System.out.println("Address from private key is: " + key.toAddress(params).toString()); // And the address ... diff --git a/tests/com/google/bitcoin/core/AlertMessageTest.java b/tests/com/google/bitcoin/core/AlertMessageTest.java index 709c729f..f2a44822 100644 --- a/tests/com/google/bitcoin/core/AlertMessageTest.java +++ b/tests/com/google/bitcoin/core/AlertMessageTest.java @@ -20,8 +20,6 @@ import org.bouncycastle.util.encoders.Hex; import org.junit.Before; import org.junit.Test; -import java.math.BigInteger; - import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; @@ -31,7 +29,7 @@ public class AlertMessageTest { @Before public void setUp() throws Exception { - ECKey key = new ECKey(new BigInteger(1, TEST_KEY_PRIV)); + ECKey key = new ECKey(TEST_KEY_PRIV, null); params = NetworkParameters.unitTests(); params.alertSigningKey = key.getPubKey(); } diff --git a/tests/com/google/bitcoin/core/ECKeyTest.java b/tests/com/google/bitcoin/core/ECKeyTest.java index 05686312..b1bee085 100644 --- a/tests/com/google/bitcoin/core/ECKeyTest.java +++ b/tests/com/google/bitcoin/core/ECKeyTest.java @@ -20,9 +20,8 @@ import org.bouncycastle.util.encoders.Hex; import org.junit.Test; import java.math.BigInteger; -import java.security.PrivateKey; -import static com.google.bitcoin.core.Utils.*; +import static com.google.bitcoin.core.Utils.reverseBytes; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -32,7 +31,7 @@ public class ECKeyTest { // Test that we can construct an ECKey from a private key (deriving the public from the private), then signing // a message with it. BigInteger privkey = new BigInteger(1, Hex.decode("180cb41c7c600be951b5d3d0a7334acc7506173875834f7a6c4c786a28fcbb19")); - ECKey key = new ECKey(privkey); + ECKey key = new ECKey(privkey, null); byte[] message = new byte[32]; // All zeroes. byte[] output = key.sign(message); assertTrue(key.verify(message, output));