handle master private keys

This commit is contained in:
Craig Raw 2020-05-06 17:13:07 +02:00
parent 27dda91576
commit c5042cf130
13 changed files with 231 additions and 117 deletions

View file

@ -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<ChildNumber> 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<ChildNumber> 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;
}
}

View file

@ -21,68 +21,68 @@ public class OutputDescriptor {
private final String script;
private final int multisigThreshold;
private final Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys;
private final Map<ExtendedPublicKey, String> mapChildrenDerivations;
private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys;
private final Map<ExtendedKey, String> 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<ExtendedPublicKey, KeyDerivation> extendedPublicKeys) {
public OutputDescriptor(String script, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) {
this(script, 0, extendedPublicKeys);
}
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys) {
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) {
this(script, multisigThreshold, extendedPublicKeys, new LinkedHashMap<>());
}
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys, Map<ExtendedPublicKey, String> mapChildrenDerivations) {
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations) {
this.script = script;
this.multisigThreshold = multisigThreshold;
this.extendedPublicKeys = extendedPublicKeys;
this.mapChildrenDerivations = mapChildrenDerivations;
}
public Set<ExtendedPublicKey> getExtendedPublicKeys() {
public Set<ExtendedKey> 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<ChildNumber> getReceivingDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) {
public List<ChildNumber> 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<ChildNumber> getChangeDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) {
public List<ChildNumber> 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<ChildNumber> getChildDerivation(ExtendedPublicKey extendedPublicKey) {
public List<ChildNumber> getChildDerivation(ExtendedKey extendedPublicKey) {
return getChildDerivation(extendedPublicKey, 0);
}
public List<ChildNumber> getChildDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) {
public List<ChildNumber> 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<ChildNumber> getChildDerivation() {
List<ChildNumber> lastDerivation = null;
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
List<ChildNumber> 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<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null));
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
List<ChildNumber> 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<ExtendedPublicKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
Map<ExtendedPublicKey, String> keyChildDerivationMap = new LinkedHashMap<>();
Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
Map<ExtendedKey, String> 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));
}

View file

@ -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<ChildNumber> path) {
public DeterministicKey get(List<ChildNumber> path) throws HDDerivationException {
if(!keys.containsKey(path)) {
if(path.size() == 0) {
throw new IllegalArgumentException("Can't derive the master key: nothing to derive from.");

View file

@ -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<ChildNumber> 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

View file

@ -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);
}
}

View file

@ -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<ChildNumber> 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);

View file

@ -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<ExtendedPublicKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
private Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
private Map<String, String> globalProprietary = new LinkedHashMap<>();
private List<PSBTInput> 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<ExtendedPublicKey> getExtendedPublicKeys() {
return new ArrayList<ExtendedPublicKey>(extendedPublicKeys.keySet());
public List<ExtendedKey> getExtendedPublicKeys() {
return new ArrayList<ExtendedKey>(extendedPublicKeys.keySet());
}
public String toString() {

View file

@ -10,7 +10,7 @@ import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.*;
public class Bip39 {
public class Bip39Calculator {
private Map<String, Integer> wordlistIndex;
public byte[] getSeed(List<String> mnemonicWords, String passphrase) {

View file

@ -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;

View file

@ -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());

View file

@ -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());

View file

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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));
}

View file

@ -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());
}
}