From c5042cf130457233955aa4c72b1ad543bdfcb171 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 May 2020 17:13:07 +0200 Subject: [PATCH] handle master private keys --- ...xtendedPublicKey.java => ExtendedKey.java} | 105 +++++++++--------- .../drongo/OutputDescriptor.java | 60 +++++----- .../drongo/crypto/DeterministicHierarchy.java | 4 +- .../drongo/crypto/DeterministicKey.java | 13 +++ .../drongo/crypto/HDDerivationException.java | 19 ++++ .../drongo/crypto/HDKeyDerivation.java | 40 ++++++- .../com/sparrowwallet/drongo/psbt/PSBT.java | 14 +-- .../{Bip39.java => Bip39Calculator.java} | 2 +- .../sparrowwallet/drongo/wallet/Keystore.java | 32 +++++- .../drongo/OutputDescriptorTest.java | 2 +- .../sparrowwallet/drongo/psbt/PSBTTest.java | 4 +- ...ip39Test.java => Bip39CalculatorTest.java} | 30 ++--- .../drongo/wallet/KeystoreTest.java | 23 ++++ 13 files changed, 231 insertions(+), 117 deletions(-) rename src/main/java/com/sparrowwallet/drongo/{ExtendedPublicKey.java => ExtendedKey.java} (58%) create mode 100644 src/main/java/com/sparrowwallet/drongo/crypto/HDDerivationException.java rename src/main/java/com/sparrowwallet/drongo/wallet/{Bip39.java => Bip39Calculator.java} (99%) rename src/test/java/com/sparrowwallet/drongo/wallet/{Bip39Test.java => Bip39CalculatorTest.java} (78%) create mode 100644 src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java diff --git a/src/main/java/com/sparrowwallet/drongo/ExtendedPublicKey.java b/src/main/java/com/sparrowwallet/drongo/ExtendedKey.java similarity index 58% rename from src/main/java/com/sparrowwallet/drongo/ExtendedPublicKey.java rename to src/main/java/com/sparrowwallet/drongo/ExtendedKey.java index c7e4b5c..7c60780 100644 --- a/src/main/java/com/sparrowwallet/drongo/ExtendedPublicKey.java +++ b/src/main/java/com/sparrowwallet/drongo/ExtendedKey.java @@ -7,27 +7,25 @@ import com.sparrowwallet.drongo.protocol.ScriptType; import java.nio.ByteBuffer; import java.util.*; -import static com.sparrowwallet.drongo.KeyDerivation.parsePath; - -public class ExtendedPublicKey { +public class ExtendedKey { private final byte[] parentFingerprint; - private final DeterministicKey pubKey; - private final ChildNumber pubKeyChildNumber; + private final DeterministicKey key; + private final ChildNumber keyChildNumber; private final DeterministicHierarchy hierarchy; - public ExtendedPublicKey(DeterministicKey pubKey, byte[] parentFingerprint, ChildNumber pubKeyChildNumber) { + public ExtendedKey(DeterministicKey key, byte[] parentFingerprint, ChildNumber keyChildNumber) { this.parentFingerprint = parentFingerprint; - this.pubKey = pubKey; - this.pubKeyChildNumber = pubKeyChildNumber; - this.hierarchy = new DeterministicHierarchy(pubKey); + this.key = key; + this.keyChildNumber = keyChildNumber; + this.hierarchy = new DeterministicHierarchy(key); } public byte[] getParentFingerprint() { return parentFingerprint; } - public DeterministicKey getPubKey() { - return pubKey; + public DeterministicKey getKey() { + return key; } public DeterministicKey getKey(List path) { @@ -35,46 +33,51 @@ public class ExtendedPublicKey { } public String toString() { - return getExtendedPublicKey(); + return getExtendedKey(); } - public String toString(XpubHeader xpubHeader) { - return getExtendedPublicKey(xpubHeader); + public String toString(Header extendedKeyHeader) { + return getExtendedKey(extendedKeyHeader); } - public String getExtendedPublicKey() { - return Base58.encodeChecked(getExtendedPublicKeyBytes()); + public String getExtendedKey() { + return Base58.encodeChecked(getExtendedKeyBytes()); } - public String getExtendedPublicKey(XpubHeader xpubHeader) { - return Base58.encodeChecked(getExtendedPublicKeyBytes(xpubHeader)); + public String getExtendedKey(Header extendedKeyHeader) { + return Base58.encodeChecked(getExtendedKeyBytes(extendedKeyHeader)); } - public ChildNumber getPubKeyChildNumber() { - return pubKeyChildNumber; + public ChildNumber getKeyChildNumber() { + return keyChildNumber; } - public byte[] getExtendedPublicKeyBytes() { - return getExtendedPublicKeyBytes(XpubHeader.xpub); + public byte[] getExtendedKeyBytes() { + return getExtendedKeyBytes(key.isPubKeyOnly() ? Header.xpub : Header.xprv); } - public byte[] getExtendedPublicKeyBytes(XpubHeader xpubHeader) { + public byte[] getExtendedKeyBytes(Header extendedKeyHeader) { ByteBuffer buffer = ByteBuffer.allocate(78); - buffer.putInt(xpubHeader.header); - buffer.put((byte)pubKey.getDepth()); + buffer.putInt(extendedKeyHeader.header); + buffer.put((byte) key.getDepth()); buffer.put(parentFingerprint); - buffer.putInt(pubKeyChildNumber.i()); - buffer.put(pubKey.getChainCode()); - buffer.put(pubKey.getPubKey()); + buffer.putInt(keyChildNumber.i()); + buffer.put(key.getChainCode()); + if(key.isPubKeyOnly()) { + buffer.put(key.getPubKey()); + } else { + buffer.put((byte)0); + buffer.put(key.getPrivKeyBytes()); + } return buffer.array(); } - public static ExtendedPublicKey fromDescriptor(String extPubKey) { + public static ExtendedKey fromDescriptor(String extPubKey) { byte[] serializedKey = Base58.decodeChecked(extPubKey); ByteBuffer buffer = ByteBuffer.wrap(serializedKey); int header = buffer.getInt(); - if(!XpubHeader.isValidHeader(header)) { + if(!Header.isValidHeader(header)) { throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4)); } @@ -86,7 +89,7 @@ public class ExtendedPublicKey { List path; if(depth == 0) { - //Poorly formatted extended public key, add first child path element + //Poorly formatted extended key, add first child path element childNumber = new ChildNumber(0, false); } else if ((i & ChildNumber.HARDENED_BIT) != 0) { childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened @@ -104,12 +107,12 @@ public class ExtendedPublicKey { } DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint); - return new ExtendedPublicKey(pubKey, parentFingerprint, childNumber); + return new ExtendedKey(pubKey, parentFingerprint, childNumber); } public static boolean isValid(String extPubKey) { try { - ExtendedPublicKey.fromDescriptor(extPubKey); + ExtendedKey.fromDescriptor(extPubKey); } catch (Exception e) { return false; } @@ -117,16 +120,16 @@ public class ExtendedPublicKey { return true; } - public ExtendedPublicKey copy() { + public ExtendedKey copy() { //DeterministicKey is effectively final - return new ExtendedPublicKey(pubKey, Arrays.copyOf(parentFingerprint, parentFingerprint.length), pubKeyChildNumber); + return new ExtendedKey(key, Arrays.copyOf(parentFingerprint, parentFingerprint.length), keyChildNumber); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ExtendedPublicKey that = (ExtendedPublicKey) o; + ExtendedKey that = (ExtendedKey) o; return that.toString().equals(this.toString()); } @@ -135,13 +138,15 @@ public class ExtendedPublicKey { return toString().hashCode(); } - public enum XpubHeader { + public enum Header { + xprv("xprv", 0x0488ADE4, null), xpub("xpub", 0x0488B21E, ScriptType.P2PKH), ypub("ypub", 0x049D7CB2, ScriptType.P2SH_P2WPKH), zpub("zpub", 0x04B24746, ScriptType.P2WPKH), Ypub("Ypub", 0x0295b43f, ScriptType.P2SH_P2WSH), Zpub("Zpub", 0x02aa7ed3, ScriptType.P2WSH), tpub("tpub", 0x043587cf, ScriptType.P2PKH), + tprv("tprv", 0x04358394, null), upub("upub", 0x044a5262, ScriptType.P2SH_P2WPKH), vpub("vpub", 0x045f1cf6, ScriptType.P2WPKH), Upub("Upub", 0x024289ef, ScriptType.P2SH_P2WSH), @@ -151,7 +156,7 @@ public class ExtendedPublicKey { private final int header; private final ScriptType defaultScriptType; - XpubHeader(String name, int header, ScriptType defaultScriptType) { + Header(String name, int header, ScriptType defaultScriptType) { this.name = name; this.header = header; this.defaultScriptType = defaultScriptType; @@ -169,29 +174,29 @@ public class ExtendedPublicKey { return defaultScriptType; } - public static XpubHeader fromXpub(String xpub) { - for(XpubHeader xpubHeader : XpubHeader.values()) { - if(xpub.startsWith(xpubHeader.name)) { - return xpubHeader; + public static Header fromExtendedKey(String xkey) { + for(Header extendedKeyHeader : Header.values()) { + if(xkey.startsWith(extendedKeyHeader.name)) { + return extendedKeyHeader; } } - throw new IllegalArgumentException("Unrecognised xpub header for xpub: " + xpub); + throw new IllegalArgumentException("Unrecognised extended key header for extended key: " + xpub); } - public static XpubHeader fromScriptType(ScriptType scriptType) { - for(XpubHeader xpubHeader : XpubHeader.values()) { - if(xpubHeader.defaultScriptType.equals(scriptType)) { - return xpubHeader; + public static Header fromScriptType(ScriptType scriptType) { + for(Header extendedKeyHeader : Header.values()) { + if(extendedKeyHeader.defaultScriptType != null && extendedKeyHeader.defaultScriptType.equals(scriptType)) { + return extendedKeyHeader; } } - return XpubHeader.xpub; + return Header.xpub; } public static boolean isValidHeader(int header) { - for(XpubHeader xpubHeader : XpubHeader.values()) { - if(header == xpubHeader.header) { + for(Header extendedKeyHeader : Header.values()) { + if(header == extendedKeyHeader.header) { return true; } } diff --git a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java index fd2cff2..f78454e 100644 --- a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java +++ b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java @@ -21,68 +21,68 @@ public class OutputDescriptor { private final String script; private final int multisigThreshold; - private final Map extendedPublicKeys; - private final Map mapChildrenDerivations; + private final Map extendedPublicKeys; + private final Map mapChildrenDerivations; - public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey, KeyDerivation keyDerivation) { + public OutputDescriptor(String script, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) { this(script, Collections.singletonMap(extendedPublicKey, keyDerivation)); } - public OutputDescriptor(String script, Map extendedPublicKeys) { + public OutputDescriptor(String script, Map extendedPublicKeys) { this(script, 0, extendedPublicKeys); } - public OutputDescriptor(String script, int multisigThreshold, Map extendedPublicKeys) { + public OutputDescriptor(String script, int multisigThreshold, Map extendedPublicKeys) { this(script, multisigThreshold, extendedPublicKeys, new LinkedHashMap<>()); } - public OutputDescriptor(String script, int multisigThreshold, Map extendedPublicKeys, Map mapChildrenDerivations) { + public OutputDescriptor(String script, int multisigThreshold, Map extendedPublicKeys, Map mapChildrenDerivations) { this.script = script; this.multisigThreshold = multisigThreshold; this.extendedPublicKeys = extendedPublicKeys; this.mapChildrenDerivations = mapChildrenDerivations; } - public Set getExtendedPublicKeys() { + public Set getExtendedPublicKeys() { return Collections.unmodifiableSet(extendedPublicKeys.keySet()); } - public KeyDerivation getKeyDerivation(ExtendedPublicKey extendedPublicKey) { + public KeyDerivation getKeyDerivation(ExtendedKey extendedPublicKey) { return extendedPublicKeys.get(extendedPublicKey); } - public String getChildDerivationPath(ExtendedPublicKey extendedPublicKey) { + public String getChildDerivationPath(ExtendedKey extendedPublicKey) { return mapChildrenDerivations.get(extendedPublicKey); } - public boolean describesMultipleAddresses(ExtendedPublicKey extendedPublicKey) { + public boolean describesMultipleAddresses(ExtendedKey extendedPublicKey) { return getChildDerivationPath(extendedPublicKey).endsWith("/*"); } - public List getReceivingDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) { + public List getReceivingDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) { String childDerivationPath = getChildDerivationPath(extendedPublicKey); if(describesMultipleAddresses(extendedPublicKey)) { if(childDerivationPath.endsWith("0/*")) { - return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement); + return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement); } - if(extendedPublicKey.getPubKeyChildNumber().num() == 0 && childDerivationPath.endsWith("/*")) { - return getChildDerivation(new ChildNumber(0, extendedPublicKey.getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + if(extendedPublicKey.getKeyChildNumber().num() == 0 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(0, extendedPublicKey.getKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); } } throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString()); } - public List getChangeDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) { + public List getChangeDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) { String childDerivationPath = getChildDerivationPath(extendedPublicKey); if(describesMultipleAddresses(extendedPublicKey)) { if(childDerivationPath.endsWith("0/*")) { - return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement); + return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement); } - if(extendedPublicKey.getPubKeyChildNumber().num() == 1 && childDerivationPath.endsWith("/*")) { - return getChildDerivation(new ChildNumber(1, extendedPublicKey.getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + if(extendedPublicKey.getKeyChildNumber().num() == 1 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(1, extendedPublicKey.getKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); } } @@ -97,20 +97,20 @@ public class OutputDescriptor { return path; } - public List getChildDerivation(ExtendedPublicKey extendedPublicKey) { + public List getChildDerivation(ExtendedKey extendedPublicKey) { return getChildDerivation(extendedPublicKey, 0); } - public List getChildDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) { + public List getChildDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) { String childDerivationPath = getChildDerivationPath(extendedPublicKey); - return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement); + return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement); } public boolean isMultisig() { return extendedPublicKeys.size() > 1; } - public ExtendedPublicKey getSingletonExtendedPublicKey() { + public ExtendedKey getSingletonExtendedPublicKey() { if(isMultisig()) { throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested"); } @@ -123,7 +123,7 @@ public class OutputDescriptor { } public boolean describesMultipleAddresses() { - for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { + for(ExtendedKey pubKey : extendedPublicKeys.keySet()) { if(describesMultipleAddresses(pubKey)) { return false; } @@ -134,7 +134,7 @@ public class OutputDescriptor { public List getChildDerivation() { List lastDerivation = null; - for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { + for(ExtendedKey pubKey : extendedPublicKeys.keySet()) { List derivation = getChildDerivation(pubKey); if(lastDerivation != null && !lastDerivation.subList(1, lastDerivation.size()).equals(derivation.subList(1, derivation.size()))) { throw new IllegalStateException("Cannot determine multisig derivation: constituent derivations do not match"); @@ -210,7 +210,7 @@ public class OutputDescriptor { List chunks = new ArrayList<>(); chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null)); - for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { + for(ExtendedKey pubKey : extendedPublicKeys.keySet()) { List keyPath = null; if(path.get(0).num() == 0) { keyPath = getReceivingDerivation(pubKey, path.get(1).num()); @@ -258,8 +258,8 @@ public class OutputDescriptor { } private static OutputDescriptor getOutputDescriptorImpl(String script, int multisigThreshold, String descriptor) { - Map keyDerivationMap = new LinkedHashMap<>(); - Map keyChildDerivationMap = new LinkedHashMap<>(); + Map keyDerivationMap = new LinkedHashMap<>(); + Map keyChildDerivationMap = new LinkedHashMap<>(); Matcher matcher = XPUB_PATTERN.matcher(descriptor); while(matcher.find()) { String masterFingerprint = null; @@ -282,7 +282,7 @@ public class OutputDescriptor { } KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath); - ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(extPubKey); + ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey); keyDerivationMap.put(extendedPublicKey, keyDerivation); keyChildDerivationMap.put(extendedPublicKey, childDerivationPath); } @@ -298,13 +298,13 @@ public class OutputDescriptor { if(isMultisig()) { StringJoiner joiner = new StringJoiner(","); joiner.add(Integer.toString(multisigThreshold)); - for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { + for(ExtendedKey pubKey : extendedPublicKeys.keySet()) { joiner.add(pubKey.toString()); joiner.add(mapChildrenDerivations.get(pubKey)); } builder.append(joiner.toString()); } else { - ExtendedPublicKey extendedPublicKey = getSingletonExtendedPublicKey(); + ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey(); builder.append(extendedPublicKey); builder.append(mapChildrenDerivations.get(extendedPublicKey)); } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicHierarchy.java b/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicHierarchy.java index fbecf54..6f4abb6 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicHierarchy.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicHierarchy.java @@ -30,9 +30,9 @@ public class DeterministicHierarchy { * * @param path the path to the key * @return next newly created key using the child derivation function - * @throws IllegalArgumentException if create is false and the path was not found. + * @throws HDDerivationException if create is false and the path was not found. */ - public DeterministicKey get(List path) { + public DeterministicKey get(List path) throws HDDerivationException { if(!keys.containsKey(path)) { if(path.size() == 0) { throw new IllegalArgumentException("Can't derive the master key: nothing to derive from."); diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java b/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java index 2cb36b4..b66aae3 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java @@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.Base58; import com.sparrowwallet.drongo.protocol.Sha256Hash; +import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -54,6 +55,18 @@ public class DeterministicKey extends ECKey { this.parentFingerprint = (parent != null) ? parent.getFingerprint() : new byte[4]; } + public DeterministicKey(List childNumberPath, + byte[] chainCode, + BigInteger priv, + DeterministicKey parent) { + super(priv, ECKey.publicPointFromPrivate(priv), true); + this.parent = parent; + this.childNumberPath = childNumberPath; + this.chainCode = Arrays.copyOf(chainCode, chainCode.length); + this.depth = parent == null ? 0 : parent.depth + 1; + this.parentFingerprint = (parent != null) ? parent.getFingerprint() : new byte[4]; + } + /** * Return this key's depth in the hierarchy, where the root node is at depth zero. * This may be different than the number of segments in the path if this key was diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/HDDerivationException.java b/src/main/java/com/sparrowwallet/drongo/crypto/HDDerivationException.java new file mode 100644 index 0000000..0318599 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/crypto/HDDerivationException.java @@ -0,0 +1,19 @@ +package com.sparrowwallet.drongo.crypto; + +public class HDDerivationException extends RuntimeException { + public HDDerivationException() { + super(); + } + + public HDDerivationException(String message) { + super(message); + } + + public HDDerivationException(Throwable cause) { + super(cause); + } + + public HDDerivationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java b/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java index 617b7e0..eae9f0d 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java @@ -5,22 +5,52 @@ import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; +import java.util.List; public class HDKeyDerivation { - public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) { + public static final String BITCOIN_SEED_KEY = "Bitcoin seed"; + + public static DeterministicKey createMasterPrivateKey(byte[] seed) throws HDDerivationException { + byte[] hmacSha512 = Utils.getHmacSha512Hash(BITCOIN_SEED_KEY.getBytes(StandardCharsets.UTF_8), seed); + byte[] privKeyBytes = Arrays.copyOfRange(hmacSha512, 0, 32); + byte[] chainCode = Arrays.copyOfRange(hmacSha512, 32, 64); + Arrays.fill(hmacSha512, (byte)0); + DeterministicKey masterPrivKey = createMasterPrivKeyFromBytes(privKeyBytes, chainCode); + Arrays.fill(privKeyBytes, (byte)0); + Arrays.fill(chainCode, (byte)0); + return masterPrivKey; + } + + public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode) throws HDDerivationException { + // childNumberPath is an empty list because we are creating the root key. + return createMasterPrivKeyFromBytes(privKeyBytes, chainCode, Collections.emptyList()); + } + + public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode, List childNumberPath) throws HDDerivationException { + BigInteger priv = new BigInteger(1, privKeyBytes); + if(priv.equals(BigInteger.ZERO) || priv.compareTo(ECKey.CURVE.getN()) > 0) { + throw new HDDerivationException("Private key bytes are not valid"); + } + + return new DeterministicKey(childNumberPath, chainCode, priv, null); + } + + public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException { RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber); return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), parent); } - public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) { + public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException { if(childNumber.isHardened()) { - throw new IllegalArgumentException("Can't use private derivation with public keys only."); + throw new HDDerivationException("Can't use private derivation with public keys only."); } byte[] parentPublicKey = parent.getPubKeyPoint().getEncoded(true); if(parentPublicKey.length != 33) { - throw new IllegalArgumentException("Parent pubkey must be 33 bytes, but is " + parentPublicKey.length); + throw new HDDerivationException("Parent pubkey must be 33 bytes, but is " + parentPublicKey.length); } ByteBuffer data = ByteBuffer.allocate(37); @@ -28,7 +58,7 @@ public class HDKeyDerivation { data.putInt(childNumber.i()); byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array()); if(i.length != 64) { - throw new IllegalStateException("HmacSHA512 output must be 64 bytes, is" + i.length); + throw new HDDerivationException("HmacSHA512 output must be 64 bytes, is" + i.length); } byte[] il = Arrays.copyOfRange(i, 0, 32); diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index e893de9..ae9c259 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -1,6 +1,6 @@ package com.sparrowwallet.drongo.psbt; -import com.sparrowwallet.drongo.ExtendedPublicKey; +import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.*; @@ -39,7 +39,7 @@ public class PSBT { private Transaction transaction = null; private Integer version = null; - private Map extendedPublicKeys = new LinkedHashMap<>(); + private Map extendedPublicKeys = new LinkedHashMap<>(); private Map globalProprietary = new LinkedHashMap<>(); private List psbtInputs = new ArrayList<>(); @@ -221,9 +221,9 @@ public class PSBT { case PSBT_GLOBAL_BIP32_PUBKEY: entry.checkOneBytePlusXpubKey(); KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); - ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(Base58.encodeChecked(entry.getKeyData())); + ExtendedKey pubKey = ExtendedKey.fromDescriptor(Base58.encodeChecked(entry.getKeyData())); this.extendedPublicKeys.put(pubKey, keyDerivation); - log.debug("Pubkey with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + ": " + pubKey.getExtendedPublicKey()); + log.debug("Pubkey with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + ": " + pubKey.getExtendedKey()); break; case PSBT_GLOBAL_VERSION: entry.checkOneByteKey(); @@ -382,12 +382,12 @@ public class PSBT { return version; } - public KeyDerivation getKeyDerivation(ExtendedPublicKey publicKey) { + public KeyDerivation getKeyDerivation(ExtendedKey publicKey) { return extendedPublicKeys.get(publicKey); } - public List getExtendedPublicKeys() { - return new ArrayList(extendedPublicKeys.keySet()); + public List getExtendedPublicKeys() { + return new ArrayList(extendedPublicKeys.keySet()); } public String toString() { diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Bip39.java b/src/main/java/com/sparrowwallet/drongo/wallet/Bip39Calculator.java similarity index 99% rename from src/main/java/com/sparrowwallet/drongo/wallet/Bip39.java rename to src/main/java/com/sparrowwallet/drongo/wallet/Bip39Calculator.java index 309c4a3..f6188ba 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Bip39.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Bip39Calculator.java @@ -10,7 +10,7 @@ import java.nio.charset.StandardCharsets; import java.text.Normalizer; import java.util.*; -public class Bip39 { +public class Bip39Calculator { private Map wordlistIndex; public byte[] getSeed(List mnemonicWords, String passphrase) { diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java index b39e6cd..6879bec 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -1,8 +1,11 @@ package com.sparrowwallet.drongo.wallet; -import com.sparrowwallet.drongo.ExtendedPublicKey; +import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.crypto.DeterministicKey; +import com.sparrowwallet.drongo.crypto.HDKeyDerivation; public class Keystore { public static final String DEFAULT_LABEL = "Keystore 1"; @@ -11,7 +14,8 @@ public class Keystore { private KeystoreSource source = KeystoreSource.SW_WATCH; private WalletModel walletModel = WalletModel.SPARROW; private KeyDerivation keyDerivation; - private ExtendedPublicKey extendedPublicKey; + private ExtendedKey extendedPublicKey; + private byte[] seed; public Keystore() { this(DEFAULT_LABEL); @@ -57,14 +61,34 @@ public class Keystore { this.keyDerivation = keyDerivation; } - public ExtendedPublicKey getExtendedPublicKey() { + public ExtendedKey getExtendedPublicKey() { return extendedPublicKey; } - public void setExtendedPublicKey(ExtendedPublicKey extendedPublicKey) { + public void setExtendedPublicKey(ExtendedKey extendedPublicKey) { this.extendedPublicKey = extendedPublicKey; } + public byte[] getSeed() { + return seed; + } + + public void setSeed(byte[] seed) { + this.seed = seed; + } + + public DeterministicKey getMasterPrivateKey() { + if(seed == null) { + throw new IllegalArgumentException("Keystore does not contain a seed"); + } + + return HDKeyDerivation.createMasterPrivateKey(seed); + } + + public ExtendedKey getExtendedPrivateKey() { + return new ExtendedKey(getMasterPrivateKey(), new byte[4], ChildNumber.ZERO); + } + public boolean isValid() { if(label == null || source == null || walletModel == null || keyDerivation == null || extendedPublicKey == null) { return false; diff --git a/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java b/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java index dad08f1..e2916ee 100644 --- a/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java +++ b/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java @@ -45,7 +45,7 @@ public class OutputDescriptorTest { public void masterP2PKH() { OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"); Assert.assertEquals("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", descriptor.toString()); - ExtendedPublicKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey(); + ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey(); KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey); Assert.assertEquals("d34db33f", derivation.getMasterFingerprint()); Assert.assertEquals("m/44'/0'/0'", derivation.getDerivationPath()); diff --git a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java index 0e520d5..6bb27f9 100644 --- a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java +++ b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java @@ -1,6 +1,6 @@ package com.sparrowwallet.drongo.psbt; -import com.sparrowwallet.drongo.ExtendedPublicKey; +import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.protocol.NonStandardScriptException; import com.sparrowwallet.drongo.protocol.Transaction; @@ -216,7 +216,7 @@ public class PSBTTest { String psbt = "cHNidP8BAJ0BAAAAAnEOp2q0XFy2Q45gflnMA3YmmBgFrp4N/ZCJASq7C+U1AQAAAAD/////GQmU1qizyMgsy8+y+6QQaqBmObhyqNRHRlwNQliNbWcAAAAAAP////8CAOH1BQAAAAAZdqkUtrwsDuVlWoQ9ea/t0MzD991kNAmIrGBa9AUAAAAAFgAUEYjvjkzgRJ6qyPsUHL9aEXbmoIgAAAAATwEEiLIeA55TDKyAAAAAPbyKXJdp8DGxfnf+oVGGAyIaGP0Y8rmlTGyMGsdcvDUC8jBYSxVdHH8c1FEgplPEjWULQxtnxbLBPyfXFCA3wWkQJ1acUDEAAIAAAACAAAAAgAABAR8A4fUFAAAAABYAFDO5gvkbKPFgySC0q5XljOUN2jpKIgIDMJaA8zx9446mpHzU7NZvH1pJdHxv+4gI7QkDkkPjrVxHMEQCIC1wTO2DDFapCTRL10K2hS3M0QPpY7rpLTjnUlTSu0JFAiAthsQ3GV30bAztoITyopHD2i1kBw92v5uQsZXn7yj3cgEiBgMwloDzPH3jjqakfNTs1m8fWkl0fG/7iAjtCQOSQ+OtXBgnVpxQMQAAgAAAAIAAAACAAAAAAAEAAAAAAQEfAOH1BQAAAAAWABQ4j7lEMH63fvRRl9CwskXgefAR3iICAsd3Fh9z0LfHK57nveZQKT0T8JW8dlatH1Jdpf0uELEQRzBEAiBMsftfhpyULg4mEAV2ElQ5F5rojcqKncO6CPeVOYj6pgIgUh9JynkcJ9cOJzybFGFphZCTYeJb4nTqIA1+CIJ+UU0BIgYCx3cWH3PQt8crnue95lApPRPwlbx2Vq0fUl2l/S4QsRAYJ1acUDEAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgLSDKUC7iiWhtIYFb1DqAY3sGmOH7zb5MrtRF9sGgqQ7xgnVpxQMQAAgAAAAIAAAACAAAAAAAQAAAAA"; PSBT psbt1 = PSBT.fromString(psbt); - ExtendedPublicKey extendedPublicKey = psbt1.getExtendedPublicKeys().get(0); + ExtendedKey extendedPublicKey = psbt1.getExtendedPublicKeys().get(0); KeyDerivation keyDerivation = psbt1.getKeyDerivation(extendedPublicKey); Assert.assertEquals("27569c50", keyDerivation.getMasterFingerprint()); Assert.assertEquals("m/49'/0'/0'", keyDerivation.getDerivationPath()); diff --git a/src/test/java/com/sparrowwallet/drongo/wallet/Bip39Test.java b/src/test/java/com/sparrowwallet/drongo/wallet/Bip39CalculatorTest.java similarity index 78% rename from src/test/java/com/sparrowwallet/drongo/wallet/Bip39Test.java rename to src/test/java/com/sparrowwallet/drongo/wallet/Bip39CalculatorTest.java index 4db6799..f5e2a95 100644 --- a/src/test/java/com/sparrowwallet/drongo/wallet/Bip39Test.java +++ b/src/test/java/com/sparrowwallet/drongo/wallet/Bip39CalculatorTest.java @@ -7,14 +7,14 @@ import org.junit.Test; import java.util.Arrays; import java.util.List; -public class Bip39Test { +public class Bip39CalculatorTest { @Test public void bip39TwelveWordsTest() { String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; List wordlist = Arrays.asList(words.split(" ")); - Bip39 bip39 = new Bip39(); - byte[] seed = bip39.getSeed(wordlist, ""); + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(wordlist, ""); Assert.assertEquals("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd", Utils.bytesToHex(seed)); } @@ -24,8 +24,8 @@ public class Bip39Test { String words = "arch easily near social civil image seminar monkey engine party promote turtle"; List wordlist = Arrays.asList(words.split(" ")); - Bip39 bip39 = new Bip39(); - byte[] seed = bip39.getSeed(wordlist, "anotherpass867"); + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(wordlist, "anotherpass867"); Assert.assertEquals("ca50764cda44a2cf52aef3c677bebf26011f9dc2b9fddfed2a8a5a9ecb8542956990a16e6873b7724044e83708d9d3a662b765e8800e6e79b289f51c2bcad756", Utils.bytesToHex(seed)); } @@ -35,8 +35,8 @@ public class Bip39Test { String words = "open grunt omit snap behave inch engine hamster hope increase exotic segment news choose roast"; List wordlist = Arrays.asList(words.split(" ")); - Bip39 bip39 = new Bip39(); - byte[] seed = bip39.getSeed(wordlist, ""); + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(wordlist, ""); Assert.assertEquals("2174deae5fd315253dc065db7ef97f46957eb68a12505adccfb7f8aca5b63788c587e73430848f85417d9a7d95e6396d2eb3af73c9fb507ebcb9268a5ad47885", Utils.bytesToHex(seed)); } @@ -46,8 +46,8 @@ public class Bip39Test { String words = "mandate lend daring actual health dilemma throw muffin garden pony inherit volume slim visual police supreme bless crush"; List wordlist = Arrays.asList(words.split(" ")); - Bip39 bip39 = new Bip39(); - byte[] seed = bip39.getSeed(wordlist, ""); + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(wordlist, ""); Assert.assertEquals("04bd65f582e288bbf595213048b06e1552017776d20ca290ac06d840e197bcaaccd4a85a45a41219be4183dd2e521e7a7a2d6aea3069f04e503ef6d9c8dfa651", Utils.bytesToHex(seed)); } @@ -57,8 +57,8 @@ public class Bip39Test { String words = "mirror milk file hope drill conduct empty mutual physical easily sell patient green final release excuse name asset update advance resource"; List wordlist = Arrays.asList(words.split(" ")); - Bip39 bip39 = new Bip39(); - byte[] seed = bip39.getSeed(wordlist, ""); + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(wordlist, ""); Assert.assertEquals("f3a88a437153333f9759f323dfe7910e6a649c34da5800e6c978d77baad54b67b06eab17c0107243f3e8b395a2de98c910e9528127539efda2eea5ae50e94019", Utils.bytesToHex(seed)); } @@ -68,8 +68,8 @@ public class Bip39Test { String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval"; List wordlist = Arrays.asList(words.split(" ")); - Bip39 bip39 = new Bip39(); - byte[] seed = bip39.getSeed(wordlist, ""); + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(wordlist, ""); Assert.assertEquals("60f825219a1fcfa479de28435e9bf2aa5734e212982daee582ca0427ad6141c65be9863c3ce0f18e2b173083ea49dcf47d07148734a5f748ac60d470cee6a2bc", Utils.bytesToHex(seed)); } @@ -79,8 +79,8 @@ public class Bip39Test { String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval"; List wordlist = Arrays.asList(words.split(" ")); - Bip39 bip39 = new Bip39(); - byte[] seed = bip39.getSeed(wordlist, "thispass"); + Bip39Calculator bip39Calculator = new Bip39Calculator(); + byte[] seed = bip39Calculator.getSeed(wordlist, "thispass"); Assert.assertEquals("a652d123f421f56257391af26063e900619678b552dafd3850e699f6da0667269bbcaebb0509557481db29607caac0294b3cd337d740174cfa05f552fe9e0272", Utils.bytesToHex(seed)); } diff --git a/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java new file mode 100644 index 0000000..20979f1 --- /dev/null +++ b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java @@ -0,0 +1,23 @@ +package com.sparrowwallet.drongo.wallet; + +import com.sparrowwallet.drongo.Utils; +import org.junit.Assert; +import org.junit.Test; + +public class KeystoreTest { + @Test + public void testExtendedPrivateKey() { + Keystore keystore = new Keystore(); + keystore.setSeed(Utils.hexToBytes("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd")); + + Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedPrivateKey().toString()); + } + + @Test + public void testExtendedPrivateKeyTwo() { + Keystore keystore = new Keystore(); + keystore.setSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c")); + + Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedPrivateKey().toString()); + } +}