add support for dumped private keys

This commit is contained in:
Craig Raw 2021-11-29 15:30:31 +02:00
parent da14a9bf34
commit 16d348a91d
3 changed files with 208 additions and 5 deletions

View file

@ -1,14 +1,14 @@
package com.sparrowwallet.drongo; package com.sparrowwallet.drongo;
public enum Network { public enum Network {
MAINNET("mainnet", 0, "1", 5, "3", "bc", ExtendedKey.Header.xprv, ExtendedKey.Header.xpub, 8332), MAINNET("mainnet", 0, "1", 5, "3", "bc", ExtendedKey.Header.xprv, ExtendedKey.Header.xpub, 128, 8332),
TESTNET("testnet", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 18332), TESTNET("testnet", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 18332),
REGTEST("regtest", 111, "mn", 196, "2", "bcrt", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 18443), REGTEST("regtest", 111, "mn", 196, "2", "bcrt", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 18443),
SIGNET("signet", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 38332); SIGNET("signet", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub, 239, 38332);
public static final String BLOCK_HEIGHT_PROPERTY = "com.sparrowwallet.blockHeight"; public static final String BLOCK_HEIGHT_PROPERTY = "com.sparrowwallet.blockHeight";
Network(String name, int p2pkhAddressHeader, String p2pkhAddressPrefix, int p2shAddressHeader, String p2shAddressPrefix, String bech32AddressHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader, int defaultPort) { Network(String name, int p2pkhAddressHeader, String p2pkhAddressPrefix, int p2shAddressHeader, String p2shAddressPrefix, String bech32AddressHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader, int dumpedPrivateKeyHeader, int defaultPort) {
this.name = name; this.name = name;
this.p2pkhAddressHeader = p2pkhAddressHeader; this.p2pkhAddressHeader = p2pkhAddressHeader;
this.p2pkhAddressPrefix = p2pkhAddressPrefix; this.p2pkhAddressPrefix = p2pkhAddressPrefix;
@ -17,6 +17,7 @@ public enum Network {
this.bech32AddressHrp = bech32AddressHrp; this.bech32AddressHrp = bech32AddressHrp;
this.xprvHeader = xprvHeader; this.xprvHeader = xprvHeader;
this.xpubHeader = xpubHeader; this.xpubHeader = xpubHeader;
this.dumpedPrivateKeyHeader = dumpedPrivateKeyHeader;
this.defaultPort = defaultPort; this.defaultPort = defaultPort;
} }
@ -28,6 +29,7 @@ public enum Network {
private final String bech32AddressHrp; private final String bech32AddressHrp;
private final ExtendedKey.Header xprvHeader; private final ExtendedKey.Header xprvHeader;
private final ExtendedKey.Header xpubHeader; private final ExtendedKey.Header xpubHeader;
private final int dumpedPrivateKeyHeader;
private final int defaultPort; private final int defaultPort;
private static Network currentNetwork; private static Network currentNetwork;
@ -56,6 +58,10 @@ public enum Network {
return xpubHeader; return xpubHeader;
} }
public int getDumpedPrivateKeyHeader() {
return dumpedPrivateKeyHeader;
}
public int getDefaultPort() { public int getDefaultPort() {
return defaultPort; return defaultPort;
} }

View file

@ -0,0 +1,88 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Network;
import java.util.Arrays;
import java.util.Objects;
/**
* 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. 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 final boolean compressed;
/**
* Construct a private key from its Base58 representation.
* @param base58
* The textual form of the private key.
*/
public static DumpedPrivateKey fromBase58(String base58) {
return new DumpedPrivateKey(base58);
}
// Used by ECKey.getPrivateKeyEncoded()
DumpedPrivateKey(byte[] keyBytes, boolean compressed) {
super(Network.get().getDumpedPrivateKeyHeader(), encode(keyBytes, compressed));
this.compressed = compressed;
}
private static byte[] encode(byte[] keyBytes, boolean compressed) {
if(keyBytes.length != 32) {
throw new IllegalArgumentException("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;
}
}
private DumpedPrivateKey(String encoded) {
super(encoded);
if(version != Network.get().getDumpedPrivateKeyHeader())
throw new IllegalArgumentException("Invalid version " + version + " for network " + Network.get());
if(bytes.length == 33 && bytes[32] == 1) {
compressed = true;
bytes = Arrays.copyOf(bytes, 32); // Chop off the additional marker byte.
} else if(bytes.length == 32) {
compressed = false;
} else {
throw new IllegalArgumentException("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 ECKey.fromPrivate(bytes);
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
if(!super.equals(o)) {
return false;
}
DumpedPrivateKey that = (DumpedPrivateKey) o;
return compressed == that.compressed;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), compressed);
}
}

View file

@ -0,0 +1,109 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Base58;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
/**
* <p>In Bitcoin the following format is often used to represent some type of key:</p>
* <p/>
* <pre>[one version byte] [data bytes] [4 checksum bytes]</pre>
* <p/>
* <p>and the result is then Base58 encoded. This format is used for addresses, and private keys exported using the
* dumpprivkey command.</p>
*/
public class VersionedChecksummedBytes implements Serializable, Cloneable, Comparable<VersionedChecksummedBytes> {
protected final int version;
protected byte[] bytes;
protected VersionedChecksummedBytes(String encoded) {
byte[] versionAndDataBytes = Base58.decodeChecked(encoded);
byte versionByte = versionAndDataBytes[0];
version = versionByte & 0xFF;
bytes = new byte[versionAndDataBytes.length - 1];
System.arraycopy(versionAndDataBytes, 1, bytes, 0, versionAndDataBytes.length - 1);
}
protected VersionedChecksummedBytes(int version, byte[] bytes) {
this.version = version;
this.bytes = bytes;
}
/**
* Returns the base-58 encoded String representation of this
* object, including version and checksum bytes.
*/
public final String toBase58() {
// A stringified buffer is:
// 1 byte version + data bytes + 4 bytes check code (a truncated hash)
byte[] addressBytes = new byte[1 + bytes.length + 4];
addressBytes[0] = (byte) version;
System.arraycopy(bytes, 0, addressBytes, 1, bytes.length);
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, bytes.length + 1);
System.arraycopy(checksum, 0, addressBytes, bytes.length + 1, 4);
return Base58.encode(addressBytes);
}
@Override
public String toString() {
return toBase58();
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
VersionedChecksummedBytes that = (VersionedChecksummedBytes) o;
return version == that.version && Arrays.equals(bytes, that.bytes);
}
@Override
public int hashCode() {
int result = Objects.hash(version);
result = 31 * result + Arrays.hashCode(bytes);
return result;
}
/**
* {@inheritDoc}
*
* This implementation narrows the return type to <code>VersionedChecksummedBytes</code>
* and allows subclasses to throw <code>CloneNotSupportedException</code> even though it
* is never thrown by this implementation.
*/
@Override
public VersionedChecksummedBytes clone() throws CloneNotSupportedException {
return (VersionedChecksummedBytes) super.clone();
}
/**
* {@inheritDoc}
*
* This implementation uses an optimized Google Guava method to compare <code>bytes</code>.
*/
@Override
public int compareTo(VersionedChecksummedBytes o) {
int result = Integer.compare(this.version, o.version);
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
return result != 0 ? result : lexicographicByteArrayComparator.compare(this.bytes, o.bytes);
}
/**
* Returns the "version" or "header" byte: the first byte of the data. This is used to disambiguate what the
* contents apply to, for example, which network the key or address is valid on.
*
* @return A positive number between 0 and 255.
*/
public int getVersion() {
return version;
}
}