From 90662e9238aa5f0ad15137819c1bdd4877f545ed Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 11 Feb 2013 12:18:16 +0100 Subject: [PATCH] Support compressed pubkeys in the dumpprivkey format encoder/decoder. Resolves issue 308. --- .../google/bitcoin/core/DumpedPrivateKey.java | 39 +++++++++++++++---- .../java/com/google/bitcoin/core/ECKey.java | 30 +++++++++++--- .../com/google/bitcoin/core/ECKeyTest.java | 13 +++++++ 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/com/google/bitcoin/core/DumpedPrivateKey.java b/core/src/main/java/com/google/bitcoin/core/DumpedPrivateKey.java index 82b14d45..6ed16738 100644 --- a/core/src/main/java/com/google/bitcoin/core/DumpedPrivateKey.java +++ b/core/src/main/java/com/google/bitcoin/core/DumpedPrivateKey.java @@ -16,19 +16,36 @@ package com.google.bitcoin.core; +import com.google.common.base.Preconditions; +import org.spongycastle.util.Arrays; + import java.math.BigInteger; /** * Parses and generates private keys in the form used by the Bitcoin "dumpprivkey" command. This is the private key - * bytes with a header byte and 4 checksum bytes at the end. + * bytes with a header byte and 4 checksum bytes at the end. If there are 33 private key bytes instead of 32, then + * the last byte is a discriminator value for the compressed pubkey. */ public class DumpedPrivateKey extends VersionedChecksummedBytes { + private boolean compressed; + // Used by ECKey.getPrivateKeyEncoded() - DumpedPrivateKey(NetworkParameters params, byte[] keyBytes) { - super(params.dumpedPrivateKeyHeader, keyBytes); - if (keyBytes.length != 32) // 256 bit keys - throw new RuntimeException("Keys are 256 bits, so you must provide 32 bytes, got " + - keyBytes.length + " bytes"); + DumpedPrivateKey(NetworkParameters params, byte[] keyBytes, boolean compressed) { + super(params.dumpedPrivateKeyHeader, encode(keyBytes, compressed)); + this.compressed = compressed; + } + + private static byte[] encode(byte[] keyBytes, boolean compressed) { + Preconditions.checkArgument(keyBytes.length == 32, "Private keys must be 32 bytes"); + if (!compressed) { + return keyBytes; + } else { + // Keys that have compressed public components have an extra 1 byte on the end in dumped form. + byte[] bytes = new byte[33]; + System.arraycopy(keyBytes, 0, bytes, 0, 32); + bytes[32] = 1; + return bytes; + } } /** @@ -43,12 +60,20 @@ public class DumpedPrivateKey extends VersionedChecksummedBytes { if (params != null && version != params.dumpedPrivateKeyHeader) throw new AddressFormatException("Mismatched version number, trying to cross networks? " + version + " vs " + params.dumpedPrivateKeyHeader); + if (bytes.length == 33) { + compressed = true; + bytes = Arrays.copyOf(bytes, 32); // Chop off the additional marker byte. + } else if (bytes.length == 32) { + compressed = false; + } else { + throw new AddressFormatException("Wrong number of bytes for a private key, not 32 or 33"); + } } /** * 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, compressed); } } diff --git a/core/src/main/java/com/google/bitcoin/core/ECKey.java b/core/src/main/java/com/google/bitcoin/core/ECKey.java index 8fd829fb..13222f38 100644 --- a/core/src/main/java/com/google/bitcoin/core/ECKey.java +++ b/core/src/main/java/com/google/bitcoin/core/ECKey.java @@ -100,12 +100,16 @@ public class ECKey implements Serializable { // Unfortunately Bouncy Castle does not let us explicitly change a point to be compressed, even though it // could easily do so. We must re-build it here so the ECPoints withCompression flag can be set to true. ECPoint uncompressed = pubParams.getQ(); - ECPoint compressed = new ECPoint.Fp(ecParams.getCurve(), uncompressed.getX(), uncompressed.getY(), true); + ECPoint compressed = compressPoint(uncompressed); pub = compressed.getEncoded(); creationTimeSeconds = Utils.now().getTime() / 1000; } + private static ECPoint compressPoint(ECPoint uncompressed) { + return new ECPoint.Fp(ecParams.getCurve(), uncompressed.getX(), uncompressed.getY(), true); + } + /** * 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. Note that this is slow because it requires an EC point multiply. @@ -145,13 +149,14 @@ public class ECKey implements Serializable { * 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. + * @param compressed If set to true and pubKey is null, the derived public key will be in compressed form. */ - private ECKey(BigInteger privKey, byte[] pubKey) { + public ECKey(BigInteger privKey, byte[] pubKey, boolean compressed) { this.priv = privKey; this.pub = null; if (pubKey == null && privKey != null) { // Derive public from private. - this.pub = publicKeyFromPrivate(privKey); + this.pub = publicKeyFromPrivate(privKey, compressed); } else if (pubKey != null) { // We expect the pubkey to be in regular encoded form, just as a BigInteger. Therefore the first byte is // a special marker byte. @@ -159,6 +164,16 @@ public class ECKey implements Serializable { this.pub = pubKey; } } + + /** + * 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. + */ + private ECKey(BigInteger privKey, byte[] pubKey) { + this(privKey, pubKey, false); + } /** Creates an ECKey given the private key only. The public key is calculated from it (this is slow) */ public ECKey(BigInteger privKey) { @@ -183,8 +198,11 @@ public class ECKey implements Serializable { * 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(); + public static byte[] publicKeyFromPrivate(BigInteger privKey, boolean compressed) { + ECPoint point = ecParams.getG().multiply(privKey); + if (compressed) + point = compressPoint(point); + return point.getEncoded(); } /** Gets the hash160 form of the public key (as seen in addresses). */ @@ -545,7 +563,7 @@ public class ECKey implements Serializable { * @return Private key bytes as a {@link DumpedPrivateKey}. */ public DumpedPrivateKey getPrivateKeyEncoded(NetworkParameters params) { - return new DumpedPrivateKey(params, getPrivKeyBytes()); + return new DumpedPrivateKey(params, getPrivKeyBytes(), isCompressed()); } /** diff --git a/core/src/test/java/com/google/bitcoin/core/ECKeyTest.java b/core/src/test/java/com/google/bitcoin/core/ECKeyTest.java index 9b1c5b75..7c8f89dc 100644 --- a/core/src/test/java/com/google/bitcoin/core/ECKeyTest.java +++ b/core/src/test/java/com/google/bitcoin/core/ECKeyTest.java @@ -21,6 +21,7 @@ import org.spongycastle.util.encoders.Hex; import java.math.BigInteger; import java.security.SignatureException; +import java.util.Arrays; import static com.google.bitcoin.core.Utils.reverseBytes; import static org.junit.Assert.*; @@ -173,4 +174,16 @@ public class ECKeyTest { } assertTrue(found); } + + @Test + public void roundTripDumpedPrivKey() throws Exception { + ECKey key = new ECKey(); + assertTrue(key.isCompressed()); + NetworkParameters params = NetworkParameters.unitTests(); + String base58 = key.getPrivateKeyEncoded(params).toString(); + ECKey key2 = new DumpedPrivateKey(params, base58).getKey(); + assertTrue(key2.isCompressed()); + assertTrue(Arrays.equals(key.getPrivKeyBytes(), key2.getPrivKeyBytes())); + assertTrue(Arrays.equals(key.getPubKey(), key2.getPubKey())); + } }