support extended master private key keystores

This commit is contained in:
Craig Raw 2021-04-20 08:27:26 +02:00
parent 1aeaacaf59
commit 85e8b97a8c
4 changed files with 210 additions and 25 deletions

View file

@ -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,16 +96,28 @@ public class Keystore {
this.seed = seed;
}
public boolean hasPrivateKey() {
return hasSeed() || hasMasterPrivateExtendedKey();
}
public DeterministicKey getMasterPrivateKey() throws MnemonicException {
if(seed == null) {
throw new IllegalArgumentException("Keystore does not contain a seed");
if(seed == null && masterPrivateExtendedKey == null) {
throw new IllegalArgumentException("Keystore does not contain a master private key, or seed to derive one from");
}
if(seed.isEncrypted()) {
throw new IllegalArgumentException("Seed is encrypted");
if(seed != null) {
if(seed.isEncrypted()) {
throw new IllegalArgumentException("Seed is encrypted");
}
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
}
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
if(masterPrivateExtendedKey.isEncrypted()) {
throw new IllegalArgumentException("Master private key is encrypted");
}
return masterPrivateExtendedKey.getPrivateKey();
}
public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException {
@ -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();
}
}
}

View file

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

View file

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

View file

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