mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56:44 +00:00
support extended master private key keystores
This commit is contained in:
parent
1aeaacaf59
commit
85e8b97a8c
4 changed files with 210 additions and 25 deletions
|
@ -17,6 +17,7 @@ public class Keystore {
|
|||
private WalletModel walletModel = WalletModel.SPARROW;
|
||||
private KeyDerivation keyDerivation;
|
||||
private ExtendedKey extendedPublicKey;
|
||||
private MasterPrivateExtendedKey masterPrivateExtendedKey;
|
||||
private DeterministicSeed seed;
|
||||
|
||||
public Keystore() {
|
||||
|
@ -71,6 +72,22 @@ public class Keystore {
|
|||
this.extendedPublicKey = extendedPublicKey;
|
||||
}
|
||||
|
||||
public boolean hasMasterPrivateExtendedKey() {
|
||||
return masterPrivateExtendedKey != null;
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey getMasterPrivateExtendedKey() {
|
||||
return masterPrivateExtendedKey;
|
||||
}
|
||||
|
||||
public void setMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey) {
|
||||
this.masterPrivateExtendedKey = masterPrivateExtendedKey;
|
||||
}
|
||||
|
||||
public boolean hasSeed() {
|
||||
return seed != null;
|
||||
}
|
||||
|
||||
public DeterministicSeed getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
@ -79,11 +96,16 @@ public class Keystore {
|
|||
this.seed = seed;
|
||||
}
|
||||
|
||||
public DeterministicKey getMasterPrivateKey() throws MnemonicException {
|
||||
if(seed == null) {
|
||||
throw new IllegalArgumentException("Keystore does not contain a seed");
|
||||
public boolean hasPrivateKey() {
|
||||
return hasSeed() || hasMasterPrivateExtendedKey();
|
||||
}
|
||||
|
||||
public DeterministicKey getMasterPrivateKey() throws MnemonicException {
|
||||
if(seed == null && masterPrivateExtendedKey == null) {
|
||||
throw new IllegalArgumentException("Keystore does not contain a master private key, or seed to derive one from");
|
||||
}
|
||||
|
||||
if(seed != null) {
|
||||
if(seed.isEncrypted()) {
|
||||
throw new IllegalArgumentException("Seed is encrypted");
|
||||
}
|
||||
|
@ -91,6 +113,13 @@ public class Keystore {
|
|||
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
|
||||
}
|
||||
|
||||
if(masterPrivateExtendedKey.isEncrypted()) {
|
||||
throw new IllegalArgumentException("Master private key is encrypted");
|
||||
}
|
||||
|
||||
return masterPrivateExtendedKey.getPrivateKey();
|
||||
}
|
||||
|
||||
public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException {
|
||||
return new ExtendedKey(getMasterPrivateKey(), new byte[4], ChildNumber.ZERO);
|
||||
}
|
||||
|
@ -178,11 +207,11 @@ public class Keystore {
|
|||
}
|
||||
|
||||
if(source == KeystoreSource.SW_SEED) {
|
||||
if(seed == null) {
|
||||
throw new InvalidKeystoreException("Source of " + source + " but no seed is present");
|
||||
if(seed == null && masterPrivateExtendedKey == null) {
|
||||
throw new InvalidKeystoreException("Source of " + source + " but no seed or master private key is present");
|
||||
}
|
||||
|
||||
if(!seed.isEncrypted()) {
|
||||
if((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted())) {
|
||||
try {
|
||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||
|
@ -208,6 +237,9 @@ public class Keystore {
|
|||
if(extendedPublicKey != null) {
|
||||
copy.setExtendedPublicKey(extendedPublicKey.copy());
|
||||
}
|
||||
if(masterPrivateExtendedKey != null) {
|
||||
copy.setMasterPrivateExtendedKey(masterPrivateExtendedKey.copy());
|
||||
}
|
||||
if(seed != null) {
|
||||
copy.setSeed(seed.copy());
|
||||
}
|
||||
|
@ -217,50 +249,69 @@ public class Keystore {
|
|||
public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) throws MnemonicException {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setSeed(seed);
|
||||
keystore.setLabel(seed.getType().name());
|
||||
rederiveKeystoreFromMaster(keystore, derivation);
|
||||
return keystore;
|
||||
}
|
||||
|
||||
public static Keystore fromMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey, List<ChildNumber> derivation) throws MnemonicException {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey);
|
||||
keystore.setLabel("Master Key");
|
||||
rederiveKeystoreFromMaster(keystore, derivation);
|
||||
return keystore;
|
||||
}
|
||||
|
||||
private static void rederiveKeystoreFromMaster(Keystore keystore, List<ChildNumber> derivation) throws MnemonicException {
|
||||
ExtendedKey xprv = keystore.getExtendedMasterPrivateKey();
|
||||
String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint());
|
||||
DeterministicKey derivedKey = xprv.getKey(derivation);
|
||||
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
|
||||
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.isEmpty() ? ChildNumber.ZERO : derivation.get(derivation.size() - 1));
|
||||
|
||||
keystore.setLabel(seed.getType().name());
|
||||
keystore.setSource(KeystoreSource.SW_SEED);
|
||||
keystore.setWalletModel(WalletModel.SPARROW);
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
public boolean hasSeed() {
|
||||
return seed != null;
|
||||
}
|
||||
|
||||
public boolean isEncrypted() {
|
||||
return seed != null && seed.isEncrypted();
|
||||
return (seed != null && seed.isEncrypted()) || (masterPrivateExtendedKey != null && masterPrivateExtendedKey.isEncrypted());
|
||||
}
|
||||
|
||||
public void encrypt(Key key) {
|
||||
if(hasSeed() && !seed.isEncrypted()) {
|
||||
seed = seed.encrypt(key);
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey() && !masterPrivateExtendedKey.isEncrypted()) {
|
||||
masterPrivateExtendedKey = masterPrivateExtendedKey.encrypt(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void decrypt(CharSequence password) {
|
||||
if(hasSeed() && seed.isEncrypted()) {
|
||||
seed = seed.decrypt(password);
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
|
||||
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(password);
|
||||
}
|
||||
}
|
||||
|
||||
public void decrypt(Key key) {
|
||||
if(hasSeed() && seed.isEncrypted()) {
|
||||
seed = seed.decrypt(key);
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
|
||||
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearPrivate() {
|
||||
if(hasSeed()) {
|
||||
seed.clear();
|
||||
}
|
||||
if(hasMasterPrivateExtendedKey()) {
|
||||
masterPrivateExtendedKey.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class MasterPrivateExtendedKey implements EncryptableItem {
|
||||
private final byte[] privateKey;
|
||||
private final byte[] chainCode;
|
||||
|
||||
private final EncryptedData encryptedKey;
|
||||
|
||||
public MasterPrivateExtendedKey(byte[] privateKey, byte[] chainCode) {
|
||||
this.privateKey = privateKey;
|
||||
this.chainCode = chainCode;
|
||||
this.encryptedKey = null;
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey(EncryptedData encryptedKey) {
|
||||
this.privateKey = null;
|
||||
this.chainCode = null;
|
||||
this.encryptedKey = encryptedKey;
|
||||
}
|
||||
|
||||
public DeterministicKey getPrivateKey() {
|
||||
return HDKeyDerivation.createMasterPrivKeyFromBytes(privateKey, chainCode);
|
||||
}
|
||||
|
||||
public ExtendedKey getExtendedPrivateKey() {
|
||||
return new ExtendedKey(getPrivateKey(), new byte[4], ChildNumber.ZERO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
if((privateKey != null || chainCode != null) && encryptedKey != null) {
|
||||
throw new IllegalStateException("Cannot be in a encrypted and unencrypted state");
|
||||
}
|
||||
|
||||
return encryptedKey != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSecretBytes() {
|
||||
if(privateKey == null || chainCode == null) {
|
||||
throw new IllegalStateException("Cannot get secret bytes for null or encrypted key");
|
||||
}
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
|
||||
byteBuffer.put(privateKey);
|
||||
byteBuffer.put(chainCode);
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptedData getEncryptedData() {
|
||||
return encryptedKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptionType getEncryptionType() {
|
||||
return new EncryptionType(EncryptionType.Deriver.ARGON2, EncryptionType.Crypter.AES_CBC_PKCS7);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCreationTimeSeconds() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey encrypt(Key key) {
|
||||
if(encryptedKey != null) {
|
||||
throw new IllegalArgumentException("Trying to encrypt twice");
|
||||
}
|
||||
if(privateKey == null || chainCode == null) {
|
||||
throw new IllegalArgumentException("Private key data missing so cannot encrypt");
|
||||
}
|
||||
|
||||
KeyCrypter keyCrypter = getEncryptionType().getCrypter().getKeyCrypter();
|
||||
byte[] secretBytes = getSecretBytes();
|
||||
EncryptedData encryptedKeyData = keyCrypter.encrypt(secretBytes, null, key);
|
||||
Arrays.fill(secretBytes != null ? secretBytes : new byte[0], (byte)0);
|
||||
|
||||
return new MasterPrivateExtendedKey(encryptedKeyData);
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey decrypt(CharSequence password) {
|
||||
if(!isEncrypted()) {
|
||||
throw new IllegalStateException("Cannot decrypt unencrypted master private key");
|
||||
}
|
||||
|
||||
KeyDeriver keyDeriver = getEncryptionType().getDeriver().getKeyDeriver(encryptedKey.getKeySalt());
|
||||
Key key = keyDeriver.deriveKey(password);
|
||||
MasterPrivateExtendedKey seed = decrypt(key);
|
||||
key.clear();
|
||||
|
||||
return seed;
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey decrypt(Key key) {
|
||||
if(!isEncrypted()) {
|
||||
throw new IllegalStateException("Cannot decrypt unencrypted master private key");
|
||||
}
|
||||
|
||||
KeyCrypter keyCrypter = getEncryptionType().getCrypter().getKeyCrypter();
|
||||
byte[] decrypted = keyCrypter.decrypt(encryptedKey, key);
|
||||
try {
|
||||
return new MasterPrivateExtendedKey(Arrays.copyOfRange(decrypted, 0, 32), Arrays.copyOfRange(decrypted, 32, 64));
|
||||
} finally {
|
||||
Arrays.fill(decrypted, (byte)0);
|
||||
}
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey copy() {
|
||||
if(isEncrypted()) {
|
||||
return new MasterPrivateExtendedKey(encryptedKey.copy());
|
||||
}
|
||||
|
||||
return new MasterPrivateExtendedKey(Arrays.copyOf(privateKey, 32), Arrays.copyOf(chainCode, 32));
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
if(privateKey != null) {
|
||||
Arrays.fill(privateKey, (byte)0);
|
||||
}
|
||||
if(chainCode != null) {
|
||||
Arrays.fill(chainCode, (byte)0);
|
||||
}
|
||||
}
|
||||
|
||||
public MasterPrivateExtendedKey fromXprv(ExtendedKey xprv) {
|
||||
return new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
|
||||
}
|
||||
}
|
|
@ -815,7 +815,7 @@ public class Wallet {
|
|||
public void sign(PSBT psbt) throws MnemonicException {
|
||||
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
|
||||
for(Keystore keystore : getKeystores()) {
|
||||
if(keystore.hasSeed()) {
|
||||
if(keystore.hasPrivateKey()) {
|
||||
for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) {
|
||||
ECKey privKey = keystore.getKey(signingEntry.getValue());
|
||||
PSBTInput psbtInput = signingEntry.getKey();
|
||||
|
@ -1061,9 +1061,9 @@ public class Wallet {
|
|||
return copy;
|
||||
}
|
||||
|
||||
public boolean containsSeeds() {
|
||||
public boolean containsPrivateKeys() {
|
||||
for(Keystore keystore : keystores) {
|
||||
if(keystore.hasSeed()) {
|
||||
if(keystore.hasPrivateKey()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,27 +16,27 @@ public class PolicyTest {
|
|||
Keystore keystore3 = new Keystore("Keystore 3");
|
||||
|
||||
Policy policy = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, List.of(keystore1), 1);
|
||||
Assert.assertEquals("pkh(keystore1)", policy.getMiniscript().toString());
|
||||
Assert.assertEquals("pkh(keystore1)", policy.getMiniscript().toString().toLowerCase());
|
||||
Assert.assertEquals(1, policy.getNumSignaturesRequired());
|
||||
|
||||
Policy policy2 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, List.of(keystore1), 1);
|
||||
Assert.assertEquals("sh(wpkh(keystore1))", policy2.getMiniscript().toString());
|
||||
Assert.assertEquals("sh(wpkh(keystore1))", policy2.getMiniscript().toString().toLowerCase());
|
||||
Assert.assertEquals(1, policy2.getNumSignaturesRequired());
|
||||
|
||||
Policy policy3 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, List.of(keystore1), 1);
|
||||
Assert.assertEquals("wpkh(keystore1)", policy3.getMiniscript().toString());
|
||||
Assert.assertEquals("wpkh(keystore1)", policy3.getMiniscript().toString().toLowerCase());
|
||||
Assert.assertEquals(1, policy3.getNumSignaturesRequired());
|
||||
|
||||
Policy policy4 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, List.of(keystore1, keystore2, keystore3), 2);
|
||||
Assert.assertEquals("sh(sortedmulti(2,keystore1,keystore2,keystore3))", policy4.getMiniscript().toString());
|
||||
Assert.assertEquals("sh(sortedmulti(2,keystore1,keystore2,keystore3))", policy4.getMiniscript().toString().toLowerCase());
|
||||
Assert.assertEquals(2, policy4.getNumSignaturesRequired());
|
||||
|
||||
Policy policy5 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, List.of(keystore1, keystore2, keystore3), 2);
|
||||
Assert.assertEquals("sh(wsh(sortedmulti(2,keystore1,keystore2,keystore3)))", policy5.getMiniscript().toString());
|
||||
Assert.assertEquals("sh(wsh(sortedmulti(2,keystore1,keystore2,keystore3)))", policy5.getMiniscript().toString().toLowerCase());
|
||||
Assert.assertEquals(2, policy5.getNumSignaturesRequired());
|
||||
|
||||
Policy policy6 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, List.of(keystore1, keystore2, keystore3), 2);
|
||||
Assert.assertEquals("wsh(sortedmulti(2,keystore1,keystore2,keystore3))", policy6.getMiniscript().toString());
|
||||
Assert.assertEquals("wsh(sortedmulti(2,keystore1,keystore2,keystore3))", policy6.getMiniscript().toString().toLowerCase());
|
||||
Assert.assertEquals(2, policy6.getNumSignaturesRequired());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue