diff --git a/src/main/java/com/sparrowwallet/drongo/ExtendedKey.java b/src/main/java/com/sparrowwallet/drongo/ExtendedKey.java index 7c60780..232e41b 100644 --- a/src/main/java/com/sparrowwallet/drongo/ExtendedKey.java +++ b/src/main/java/com/sparrowwallet/drongo/ExtendedKey.java @@ -139,17 +139,25 @@ public class ExtendedKey { } public enum Header { - xprv("xprv", 0x0488ADE4, null), + xprv("xprv", 0x0488ADE4, ScriptType.P2PKH), xpub("xpub", 0x0488B21E, ScriptType.P2PKH), + yprv("yprv", 0x049D7878, ScriptType.P2SH_P2WPKH), ypub("ypub", 0x049D7CB2, ScriptType.P2SH_P2WPKH), + zprv("zprv", 0x04b2430c, ScriptType.P2WPKH), zpub("zpub", 0x04B24746, ScriptType.P2WPKH), + Yprv("Yprv", 0x0295b005, ScriptType.P2SH_P2WSH), Ypub("Ypub", 0x0295b43f, ScriptType.P2SH_P2WSH), + Zprv("Zprv", 0x02aa7a99, ScriptType.P2WSH), Zpub("Zpub", 0x02aa7ed3, ScriptType.P2WSH), tpub("tpub", 0x043587cf, ScriptType.P2PKH), - tprv("tprv", 0x04358394, null), + tprv("tprv", 0x04358394, ScriptType.P2PKH), + uprv("uprv", 0x044a4e28, ScriptType.P2SH_P2WPKH), upub("upub", 0x044a5262, ScriptType.P2SH_P2WPKH), + vprv("vprv", 0x045f18bc, ScriptType.P2WPKH), vpub("vpub", 0x045f1cf6, ScriptType.P2WPKH), + Uprv("Uprv", 0x024285b5, ScriptType.P2SH_P2WSH), Upub("Upub", 0x024289ef, ScriptType.P2SH_P2WSH), + Vprv("Vprv", 0x02575048, ScriptType.P2WSH), Vpub("Vpub", 0x02575483, ScriptType.P2WSH); private final String name; @@ -184,16 +192,20 @@ public class ExtendedKey { throw new IllegalArgumentException("Unrecognised extended key header for extended key: " + xpub); } - public static Header fromScriptType(ScriptType scriptType) { - for(Header extendedKeyHeader : Header.values()) { - if(extendedKeyHeader.defaultScriptType != null && extendedKeyHeader.defaultScriptType.equals(scriptType)) { - return extendedKeyHeader; + public static Header fromScriptType(ScriptType scriptType, boolean privateKey) { + for(Header header : Header.values()) { + if(header.defaultScriptType != null && header.defaultScriptType.equals(scriptType) && header.isPrivate() == privateKey) { + return header; } } return Header.xpub; } + private boolean isPrivate() { + return name.endsWith("prv"); + } + public static boolean isValidHeader(int header) { for(Header extendedKeyHeader : Header.values()) { if(header == extendedKeyHeader.header) { diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java index 342a1c7..1817a64 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java @@ -98,6 +98,15 @@ public class DeterministicSeed implements EncryptableItem { this.creationTimeSeconds = creationTimeSeconds; } + public boolean needPassphrase() { + if(isEncrypted()) { + throw new IllegalArgumentException("Cannot determine if passphrase is required in encrypted state"); + } + + byte[] mnemonicOnlySeed = Bip39MnemonicCode.toSeed(mnemonicCode, ""); + return Arrays.equals(mnemonicOnlySeed, seed); + } + private static byte[] getEntropy(SecureRandom random, int bits) { if(bits > MAX_SEED_ENTROPY_BITS) { throw new IllegalArgumentException("Requested entropy size too large"); @@ -188,6 +197,14 @@ public class DeterministicSeed implements EncryptableItem { return new DeterministicSeed(mnemonic, seed, passphrase, creationTimeSeconds); } + public DeterministicSeed setPassphrase(String passphrase) { + if(isEncrypted()) { + throw new UnsupportedOperationException("Cannot set passphrase on encrypted seed"); + } + + return new DeterministicSeed(mnemonicCode, seed, passphrase, creationTimeSeconds); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java index d9df900..4ee3531 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -90,10 +90,20 @@ public class Keystore { return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes()); } - public ExtendedKey getExtendedPrivateKey() { + public ExtendedKey getExtendedMasterPrivateKey() { return new ExtendedKey(getMasterPrivateKey(), new byte[4], ChildNumber.ZERO); } + public ExtendedKey getExtendedMasterPublicKey() { + return new ExtendedKey(getMasterPrivateKey().dropPrivateBytes(), new byte[4], ChildNumber.ZERO); + } + + public ExtendedKey getExtendedPrivateKey() { + List derivation = getKeyDerivation().getDerivation(); + DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation); + return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1)); + } + public boolean isValid() { if(label == null || source == null || walletModel == null || keyDerivation == null || extendedPublicKey == null) { return false; @@ -107,7 +117,19 @@ public class Keystore { return false; } - //TODO: If source is SW_SEED, check seed field is filled + if(source == KeystoreSource.SW_SEED) { + if(seed == null) { + return false; + } + + List derivation = getKeyDerivation().getDerivation(); + DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation); + DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent(); + ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1)); + if(!xpub.equals(getExtendedPublicKey())) { + return false; + } + } return true; } @@ -131,7 +153,7 @@ public class Keystore { public static Keystore fromSeed(DeterministicSeed seed, List derivation) { Keystore keystore = new Keystore(); keystore.setSeed(seed); - ExtendedKey xprv = keystore.getExtendedPrivateKey(); + ExtendedKey xprv = keystore.getExtendedMasterPrivateKey(); String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint()); DeterministicKey derivedKey = xprv.getKey(derivation); DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent(); @@ -146,6 +168,18 @@ public class Keystore { return keystore; } + public void setPassphrase(String passphrase) { + if(seed != null) { + seed = seed.setPassphrase(passphrase); + } else { + throw new UnsupportedOperationException("Cannot set passphrase on a keystore without a seed"); + } + } + + public boolean hasSeed() { + return seed != null; + } + public boolean isEncrypted() { return seed != null && seed.isEncrypted(); } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index be34359..2f1300b 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType; import org.bouncycastle.crypto.params.KeyParameter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -102,11 +103,22 @@ public class Wallet { if(!keystore.isValid()) { return false; } + if(derivationMatchesAnotherScriptType(keystore.getKeyDerivation().getDerivationPath())) { + return false; + } } return true; } + public boolean derivationMatchesAnotherScriptType(String derivationPath) { + if(scriptType != null && scriptType.getAccount(derivationPath) > -1) { + return false; + } + + return Arrays.stream(ScriptType.values()).anyMatch(scriptType -> !scriptType.equals(this.scriptType) && scriptType.getAccount(derivationPath) > -1); + } + public Wallet copy() { Wallet copy = new Wallet(name); copy.setPolicyType(policyType); diff --git a/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java index 8d2b661..6275f5b 100644 --- a/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java +++ b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java @@ -14,7 +14,7 @@ public class KeystoreTest { DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd"), Collections.emptyList(), 0); keystore.setSeed(seed); - Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedPrivateKey().toString()); + Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedMasterPrivateKey().toString()); } @Test @@ -23,7 +23,7 @@ public class KeystoreTest { DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"), Collections.emptyList(), 0); keystore.setSeed(seed); - Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedPrivateKey().toString()); + Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedMasterPrivateKey().toString()); } @Test